Merge 1ecbba6a12
into ac24a8703d
commit
9e8be49745
@ -0,0 +1,3 @@
|
||||
clairctl
|
||||
clairctl.yml
|
||||
reports/
|
@ -0,0 +1,25 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.7
|
||||
|
||||
install:
|
||||
- go get -v github.com/Masterminds/glide
|
||||
- cd $GOPATH/src/github.com/Masterminds/glide && git checkout tags/v0.12.3 && go install && cd -
|
||||
- glide install -v
|
||||
|
||||
script:
|
||||
- go build -ldflags "-X github.com/coreos/clair/cmd/clairctl/cmd.version=$(cat VERSION)" -v
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: SPeZzw212p/0iYPLVWUkLq226j19oWHBogHIlbKzkp8zNk7tdJEHMthAhka1zxS5afr+JWu3BBdduPHbnvvpXtv2axsrCXVsW7jPEcxXgfM3m/YSxQwjsGojMG318nX/E5ApY8eDhiZuHoTAP67DJZEoLteV5GKvNk7np74bMNexBxCPDIbXepBbjrEqxrUG/uVDFrmFf0LlCKVGxALK42uriaDM1tPGDy0+7zX7a3RqG9J4ROmZCQNzd9+rcur4DndrxYPCvJK3awUwXL5XzdRed24m5S8IG6Q/gMMUhUVECMNZ6/Z+KZ4CKqcQ9e9NvOtHYvO0OtPNrd4/HajZpUpfO008imSj6/g9NxdC6hJYvyK/HuRv5DsLgZukyvwroVSM5rC77FJeOmoLXfEBHo7E16I5XKGcp31NoCv5kdQuaHLxkBZk43CcnT/rraQ8cjCQAERQEh6u0fidyOvZ4vkTW30/c6H25zDejCzDcx1oHL4O8QwJwMfRSifSA8vk81saOurmQeviZK1UkpeWLxMYLpFZ8vN9kK/2Nn24ud9DN7N8bqGob13I8lJ69j9xcXuq/86vITziESZCHaYs2Hw1vSHD8vpkQxu8aSYqPXgwAKDwV9JkGV1/kcUdwpOlD/xkKunluu8Mf5K2JSm9G8xWr6foOibKsuHDn0nH+jY=
|
||||
file: clairctl
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: jgsqware/clairctl
|
||||
tags: true
|
||||
branch: master
|
||||
env:
|
||||
global:
|
||||
secure: QpHC0r/d3sUNZ4Tisu7IjRnE+exzUUdTRlWUvvepWA2/wdsvsh9IFKmPgHvmt/V46RtByc52HVwE/uE6gF8eVhRs4L4DP/gSi0TxmfNi2m+6EdL1MmFvdIVGfPZDDbFnNNXpWcNJGv5/UDJ8eke003Mtm7/qQfJqH3UzxoWnTrvsA7IHbhxomgsHOFqHU+e6fmaQ3q7neUJEci2HSPLehl11yBwMcsYyFjeW+33GVQeI0m2j1TrcldG3KN63tszIx7tHXTUrIK369nrndLjYWnc7Hi+k6Uc8vPh111/hLBG4UtxffP+/DTkhEtzxDNn3hn4zSKTMIyeKA/UhnySYchzDYOdVZoM0oVxAkbTcdTPWCxS75W1u3AW3QAGxScNEqmy5enrW+kGyRpd1zIRJ1qS2tHAVmos8N6bl5Rp9UJF5UITr9/DyQS93O9DOSY5Z6UVGI5J+MlG89Nr4XJ5NW4ak5yC5fc+9IwM58VMk8ULS3I0g/YBWsWRWHGZrHurnvcGbiXHk5S+anQsiYUV8n3OUa67ai7Duil3qiPklEhHeFZzmXrJMvLdeVxywfvIfKzSEGqDrUYw3hO9pkMzLxDQfvaNzAHWZGJ970N7apX2pnAUgPDsNb8cOnbDSjX36FICAiUZlrkpsz6O6h3Fm/2t/HDCyYH7gf6eJXypUOYc=
|
@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
@ -0,0 +1,58 @@
|
||||
# clairctl
|
||||
[![Build Status](https://travis-ci.org/jgsqware/clairctl.svg?branch=master)](https://travis-ci.org/jgsqware/clairctl)
|
||||
|
||||
> Tracking container vulnerabilities with Clair Control
|
||||
|
||||
Clairctl is a lightweight command-line tool doing the bridge between Registries as Docker Hub, Docker Registry or Quay.io, and the CoreOS vulnerability tracker, Clair.
|
||||
Clairctl will play as reverse proxy for authentication.
|
||||
|
||||
# Usage
|
||||
|
||||
[![asciicast](https://asciinema.org/a/41461.png)](https://asciinema.org/a/41461)
|
||||
|
||||
# Reporting
|
||||
|
||||
**clairctl** get vulnerabilities report from Clair and generate HTML report
|
||||
|
||||
clairctl can be used for Docker Hub and self-hosted Registry
|
||||
|
||||
# Command
|
||||
|
||||
```
|
||||
Analyze your docker image with Clair, directly from your registry.
|
||||
|
||||
Usage:
|
||||
clairctl [command]
|
||||
|
||||
Available Commands:
|
||||
analyze Analyze Docker image
|
||||
health Get Health of clairctl and underlying services
|
||||
login Log in to a Docker registry
|
||||
logout Log out from a Docker registry
|
||||
pull Pull Docker image information
|
||||
push Push Docker image to Clair
|
||||
report Generate Docker Image vulnerabilities report
|
||||
version Get Versions of clairctl and underlying services
|
||||
|
||||
Flags:
|
||||
--config string config file (default is ./.clairctl.yml)
|
||||
--log-level string log level [Panic,Fatal,Error,Warn,Info,Debug]
|
||||
|
||||
Use "clairctl [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
# Optional Configuration
|
||||
|
||||
```yaml
|
||||
clair:
|
||||
port: 6060
|
||||
healthPort: 6061
|
||||
uri: http://clair
|
||||
report:
|
||||
path: ./reports
|
||||
format: html
|
||||
```
|
||||
|
||||
# Contribution and Test
|
||||
|
||||
Go to /contrib folder
|
@ -0,0 +1 @@
|
||||
1.2.2
|
@ -0,0 +1,73 @@
|
||||
package clair
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/clair/api/v1"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/docker/reference"
|
||||
)
|
||||
|
||||
//Analyze return Clair Image analysis
|
||||
func Analyze(image reference.NamedTagged, manifest distribution.Manifest) ImageAnalysis {
|
||||
layers, err := newLayering(image)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot parse manifest")
|
||||
return ImageAnalysis{}
|
||||
}
|
||||
|
||||
switch manifest.(type) {
|
||||
case schema1.SignedManifest:
|
||||
|
||||
for _, l := range manifest.(schema1.SignedManifest).FSLayers {
|
||||
layers.digests = append(layers.digests, l.BlobSum.String())
|
||||
}
|
||||
return layers.analyze()
|
||||
case *schema1.SignedManifest:
|
||||
for _, l := range manifest.(*schema1.SignedManifest).FSLayers {
|
||||
layers.digests = append(layers.digests, l.BlobSum.String())
|
||||
}
|
||||
return layers.analyze()
|
||||
case schema2.DeserializedManifest:
|
||||
log.Debugf("json: %v", image)
|
||||
for _, l := range manifest.(schema2.DeserializedManifest).Layers {
|
||||
layers.digests = append(layers.digests, l.Digest.String())
|
||||
}
|
||||
return layers.analyze()
|
||||
case *schema2.DeserializedManifest:
|
||||
log.Debugf("json: %v", image)
|
||||
for _, l := range manifest.(*schema2.DeserializedManifest).Layers {
|
||||
layers.digests = append(layers.digests, l.Digest.String())
|
||||
}
|
||||
return layers.analyze()
|
||||
default:
|
||||
log.Fatalf("Unsupported Schema version.")
|
||||
return ImageAnalysis{}
|
||||
}
|
||||
}
|
||||
|
||||
func analyzeLayer(id string) (v1.LayerEnvelope, error) {
|
||||
|
||||
lURI := fmt.Sprintf("%v/layers/%v?vulnerabilities", uri, id)
|
||||
response, err := http.Get(lURI)
|
||||
if err != nil {
|
||||
return v1.LayerEnvelope{}, fmt.Errorf("analysing layer %v: %v", id, err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var analysis v1.LayerEnvelope
|
||||
err = json.NewDecoder(response.Body).Decode(&analysis)
|
||||
if err != nil {
|
||||
return v1.LayerEnvelope{}, fmt.Errorf("reading layer analysis: %v", err)
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
//TODO(jgsqware): should I show reponse body in case of error?
|
||||
return v1.LayerEnvelope{}, fmt.Errorf("receiving http error: %d", response.StatusCode)
|
||||
}
|
||||
|
||||
return analysis, nil
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package clair
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/clair/api/v1"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clairctl", "clair")
|
||||
|
||||
var uri string
|
||||
var healthURI string
|
||||
|
||||
//ImageAnalysis Full image analysis
|
||||
type ImageAnalysis struct {
|
||||
Registry, ImageName, Tag string
|
||||
Layers []v1.LayerEnvelope
|
||||
}
|
||||
|
||||
func (imageAnalysis ImageAnalysis) String() string {
|
||||
return imageAnalysis.Registry + "/" + imageAnalysis.ImageName + ":" + imageAnalysis.Tag
|
||||
}
|
||||
|
||||
//LastLayer return the last layer of ImageAnalysis
|
||||
func (imageAnalysis ImageAnalysis) LastLayer() *v1.Layer {
|
||||
return imageAnalysis.Layers[len(imageAnalysis.Layers)-1].Layer
|
||||
}
|
||||
|
||||
func (imageAnalysis ImageAnalysis) CountVulnerabilities(l v1.Layer) int {
|
||||
count := 0
|
||||
for _, feature := range l.Features {
|
||||
count += len(feature.Vulnerabilities)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func fmtURI(u string, port int) string {
|
||||
|
||||
if port != 0 {
|
||||
u += ":" + strconv.Itoa(port)
|
||||
}
|
||||
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||||
u = "http://" + u
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (imageAnalysis ImageAnalysis) ShortName(l v1.Layer) string {
|
||||
return xstrings.Substr(l.Name, 0, 12)
|
||||
}
|
||||
|
||||
//Config configure Clair from configFile
|
||||
func Config() {
|
||||
uri = fmtURI(viper.GetString("clair.uri"), viper.GetInt("clair.port")) + "/v1"
|
||||
healthURI = fmtURI(viper.GetString("clair.uri"), viper.GetInt("clair.healthPort")) + "/health"
|
||||
Report.Path = viper.GetString("clair.report.path")
|
||||
Report.Format = viper.GetString("clair.report.format")
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package clair
|
||||
|
||||
import "net/http"
|
||||
|
||||
//IsHealthy return Health clair result
|
||||
func IsHealthy() bool {
|
||||
log.Debug("requesting health on: " + healthURI)
|
||||
response, err := http.Get(healthURI)
|
||||
if err != nil {
|
||||
log.Errorf("requesting Clair health: %v", err)
|
||||
return false
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package clair
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/clair/api/v1"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||
)
|
||||
|
||||
type layering struct {
|
||||
image reference.NamedTagged
|
||||
digests []string
|
||||
parentID, hURL string
|
||||
}
|
||||
|
||||
func newLayering(image reference.NamedTagged) (*layering, error) {
|
||||
layer := layering{
|
||||
parentID: "",
|
||||
image: image,
|
||||
}
|
||||
|
||||
localIP, err := config.LocalServerIP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layer.hURL = fmt.Sprintf("http://%v/v2", localIP)
|
||||
if config.IsLocal {
|
||||
layer.hURL = strings.Replace(layer.hURL, "/v2", "/local", -1)
|
||||
log.Infof("using %v as local url", layer.hURL)
|
||||
}
|
||||
return &layer, nil
|
||||
}
|
||||
|
||||
func (layer *layering) pushAll() error {
|
||||
layerCount := len(layer.digests)
|
||||
|
||||
if layerCount == 0 {
|
||||
log.Warning("there is no layer to push")
|
||||
}
|
||||
for index, digest := range layer.digests {
|
||||
|
||||
if config.IsLocal {
|
||||
digest = strings.TrimPrefix(digest, "sha256:")
|
||||
}
|
||||
|
||||
lUID := xstrings.Substr(digest, 0, 12)
|
||||
log.Infof("Pushing Layer %d/%d [%v]", index+1, layerCount, lUID)
|
||||
|
||||
insertRegistryMapping(digest, layer.image.Hostname())
|
||||
payload := v1.LayerEnvelope{Layer: &v1.Layer{
|
||||
Name: digest,
|
||||
Path: blobsURI(layer.image.Hostname(), layer.image.RemoteName(), digest),
|
||||
ParentName: layer.parentID,
|
||||
Format: "Docker",
|
||||
}}
|
||||
|
||||
//FIXME Update to TLS
|
||||
if config.IsLocal {
|
||||
payload.Layer.Path += "/layer.tar"
|
||||
}
|
||||
payload.Layer.Path = strings.Replace(payload.Layer.Path, layer.image.Hostname(), layer.hURL, 1)
|
||||
if err := pushLayer(payload); err != nil {
|
||||
log.Infof("adding layer %d/%d [%v]: %v", index+1, layerCount, lUID, err)
|
||||
if err != ErrUnanalizedLayer {
|
||||
return err
|
||||
}
|
||||
layer.parentID = ""
|
||||
} else {
|
||||
layer.parentID = payload.Layer.Name
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (layers *layering) analyze() ImageAnalysis {
|
||||
c := len(layers.digests)
|
||||
res := []v1.LayerEnvelope{}
|
||||
|
||||
for i := range layers.digests {
|
||||
digest := layers.digests[c-i-1]
|
||||
if config.IsLocal {
|
||||
digest = strings.TrimPrefix(digest, "sha256:")
|
||||
}
|
||||
lShort := xstrings.Substr(digest, 0, 12)
|
||||
|
||||
if a, err := analyzeLayer(digest); err != nil {
|
||||
log.Errorf("analysing layer [%v] %d/%d: %v", lShort, i+1, c, err)
|
||||
} else {
|
||||
log.Infof("analysing layer [%v] %d/%d", lShort, i+1, c)
|
||||
res = append(res, a)
|
||||
}
|
||||
}
|
||||
return ImageAnalysis{
|
||||
Registry: xstrings.TrimPrefixSuffix(layers.image.Hostname(), "http://", "/v2"),
|
||||
ImageName: layers.image.Name(),
|
||||
Tag: layers.image.Tag(),
|
||||
Layers: res,
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package clair
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/clair/api/v1"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker/dockerdist"
|
||||
)
|
||||
|
||||
// ErrUnanalizedLayer is returned when the layer was not correctly analyzed
|
||||
var ErrUnanalizedLayer = errors.New("layer cannot be analyzed")
|
||||
|
||||
var registryMapping map[string]string
|
||||
|
||||
func Push(image reference.NamedTagged, manifest distribution.Manifest) error {
|
||||
layers, err := newLayering(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch manifest.(type) {
|
||||
case schema1.SignedManifest:
|
||||
for _, l := range manifest.(schema1.SignedManifest).FSLayers {
|
||||
layers.digests = append(layers.digests, l.BlobSum.String())
|
||||
}
|
||||
return layers.pushAll()
|
||||
case *schema1.SignedManifest:
|
||||
for _, l := range manifest.(*schema1.SignedManifest).FSLayers {
|
||||
layers.digests = append(layers.digests, l.BlobSum.String())
|
||||
}
|
||||
return layers.pushAll()
|
||||
case schema2.DeserializedManifest:
|
||||
for _, l := range manifest.(schema2.DeserializedManifest).Layers {
|
||||
layers.digests = append(layers.digests, l.Digest.String())
|
||||
}
|
||||
return layers.pushAll()
|
||||
case *schema2.DeserializedManifest:
|
||||
for _, l := range manifest.(*schema2.DeserializedManifest).Layers {
|
||||
layers.digests = append(layers.digests, l.Digest.String())
|
||||
}
|
||||
return layers.pushAll()
|
||||
default:
|
||||
return errors.New("Unsupported Schema version.")
|
||||
}
|
||||
}
|
||||
|
||||
func pushLayer(layer v1.LayerEnvelope) error {
|
||||
lJSON, err := json.Marshal(layer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshalling layer: %v", err)
|
||||
}
|
||||
|
||||
lURI := fmt.Sprintf("%v/layers", uri)
|
||||
request, err := http.NewRequest("POST", lURI, bytes.NewBuffer(lJSON))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating 'add layer' request: %v", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
response, err := (&http.Client{}).Do(request)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("pushing layer to clair: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != 201 {
|
||||
if response.StatusCode == 422 {
|
||||
return ErrUnanalizedLayer
|
||||
}
|
||||
return fmt.Errorf("receiving http error: %d", response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func blobsURI(registry string, name string, digest string) string {
|
||||
return strings.Join([]string{registry, name, "blobs", digest}, "/")
|
||||
}
|
||||
|
||||
func insertRegistryMapping(layerDigest string, registryURI string) {
|
||||
|
||||
hostURL, _ := dockerdist.GetPushURL(registryURI)
|
||||
log.Debugf("Saving %s[%s]", layerDigest, hostURL.String())
|
||||
registryMapping[layerDigest] = hostURL.String()
|
||||
}
|
||||
|
||||
//GetRegistryMapping return the registryURI corresponding to the layerID passed as parameter
|
||||
func GetRegistryMapping(layerDigest string) (string, error) {
|
||||
registryURI, present := registryMapping[layerDigest]
|
||||
if !present {
|
||||
return "", fmt.Errorf("%v mapping not found", layerDigest)
|
||||
}
|
||||
return registryURI, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registryMapping = map[string]string{}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package clair
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"text/template"
|
||||
|
||||
"github.com/coreos/clair/api/v1"
|
||||
"github.com/coreos/clair/database"
|
||||
)
|
||||
|
||||
//execute go generate ./clair
|
||||
//go:generate go-bindata -pkg clair -o templates.go templates/...
|
||||
|
||||
//Report Reporting Config value
|
||||
var Report ReportConfig
|
||||
|
||||
//ReportConfig Reporting configuration
|
||||
type ReportConfig struct {
|
||||
Path string
|
||||
Format string
|
||||
}
|
||||
|
||||
//ReportAsHTML report analysis as HTML
|
||||
func ReportAsHTML(analyzes ImageAnalysis) (string, error) {
|
||||
asset, err := Asset("templates/analysis-template.html")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("accessing template: %v", err)
|
||||
}
|
||||
|
||||
funcs := template.FuncMap{
|
||||
"vulnerabilities": vulnerabilities,
|
||||
"allVulnerabilities": allVulnerabilities,
|
||||
"sortedVulnerabilities": sortedVulnerabilities,
|
||||
}
|
||||
|
||||
templte := template.Must(template.New("analysis-template").Funcs(funcs).Parse(string(asset)))
|
||||
|
||||
var doc bytes.Buffer
|
||||
err = templte.Execute(&doc, analyzes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("rendering HTML report: %v", err)
|
||||
}
|
||||
return doc.String(), nil
|
||||
}
|
||||
|
||||
func invertedPriorities() []database.Severity {
|
||||
ip := make([]database.Severity, len(database.Severities))
|
||||
for i, j := 0, len(database.Severities)-1; i <= j; i, j = i+1, j-1 {
|
||||
ip[i], ip[j] = database.Severities[j], database.Severities[i]
|
||||
}
|
||||
return ip
|
||||
|
||||
}
|
||||
|
||||
type vulnerabilityWithFeature struct {
|
||||
v1.Vulnerability
|
||||
Feature string
|
||||
}
|
||||
|
||||
//VulnerabiliesCounts Total count of vulnerabilities by type
|
||||
type vulnerabiliesCounts map[database.Severity]int
|
||||
|
||||
//Total return to total of Vulnerabilities
|
||||
func (v vulnerabiliesCounts) Total() int {
|
||||
var c int
|
||||
for _, count := range v {
|
||||
c += count
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
//Count return count of severities in Vulnerabilities
|
||||
func (v vulnerabiliesCounts) Count(severity string) int {
|
||||
return v[database.Severity(severity)]
|
||||
}
|
||||
|
||||
//RelativeCount get the percentage of vulnerabilities of a severity
|
||||
func (v vulnerabiliesCounts) RelativeCount(severity string) float64 {
|
||||
count := v[database.Severity(severity)]
|
||||
result := float64(count) / float64(v.Total()) * 100
|
||||
return math.Ceil(result*100) / 100
|
||||
}
|
||||
|
||||
// allVulnerabilities Total count of vulnerabilities
|
||||
func allVulnerabilities(imageAnalysis ImageAnalysis) vulnerabiliesCounts {
|
||||
result := make(vulnerabiliesCounts)
|
||||
|
||||
l := imageAnalysis.Layers[len(imageAnalysis.Layers)-1]
|
||||
|
||||
for _, f := range l.Layer.Features {
|
||||
|
||||
for _, v := range f.Vulnerabilities {
|
||||
result[database.Severity(v.Severity)]++
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//Vulnerabilities return a list a vulnerabilities
|
||||
func vulnerabilities(imageAnalysis ImageAnalysis) map[database.Severity][]vulnerabilityWithFeature {
|
||||
|
||||
result := make(map[database.Severity][]vulnerabilityWithFeature)
|
||||
|
||||
l := imageAnalysis.Layers[len(imageAnalysis.Layers)-1]
|
||||
for _, f := range l.Layer.Features {
|
||||
for _, v := range f.Vulnerabilities {
|
||||
|
||||
result[database.Severity(v.Severity)] = append(result[database.Severity(v.Severity)], vulnerabilityWithFeature{Vulnerability: v, Feature: f.Name + ":" + f.Version})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SortedVulnerabilities get all vulnerabilities sorted by Severity
|
||||
func sortedVulnerabilities(imageAnalysis ImageAnalysis) []v1.Feature {
|
||||
features := []v1.Feature{}
|
||||
|
||||
l := imageAnalysis.Layers[len(imageAnalysis.Layers)-1]
|
||||
|
||||
for _, f := range l.Layer.Features {
|
||||
if len(f.Vulnerabilities) > 0 {
|
||||
vulnerabilities := []v1.Vulnerability{}
|
||||
for _, p := range invertedPriorities() {
|
||||
for _, v := range f.Vulnerabilities {
|
||||
if database.Severity(v.Severity) == p {
|
||||
vulnerabilities = append(vulnerabilities, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
nf := f
|
||||
nf.Vulnerabilities = vulnerabilities
|
||||
features = append(features, nf)
|
||||
}
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,558 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Clair Control report : {{.ImageName}}</title>
|
||||
|
||||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,600italic,400italic,300italic,300' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: ghostwhite;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
/* Typography */
|
||||
|
||||
.lead {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
/* global layout */
|
||||
|
||||
.container {
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0 -20px;
|
||||
}
|
||||
|
||||
.row:after {
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
[class*="col-"] {
|
||||
padding: 0 20px;
|
||||
float: left;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.col-6 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
/* padding: 1em; */
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
box-shadow: 0px 1px 2px #e2e2e2;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin-top: 0;
|
||||
padding-bottom: .2em;
|
||||
border-bottom: solid 1px gainsboro;
|
||||
}
|
||||
|
||||
.panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* Header */
|
||||
|
||||
.app-header {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
margin: 0 0px 0px 0px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 -2px 16px #263238;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
font-weight: lighter;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-intro {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #263238;
|
||||
background: rgba(255, 255, 255, .8);
|
||||
border-bottom: solid 1px #ECEFF1;
|
||||
}
|
||||
|
||||
.app-intro h2 {
|
||||
font-size: 2em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.summary {
|
||||
line-height: .6em;
|
||||
}
|
||||
/* report */
|
||||
|
||||
.report {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.graph {
|
||||
margin: 0 auto;
|
||||
max-width: 960px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
/* Style of the graph */
|
||||
|
||||
.graph .node {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.graph .node .dot {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border-radius: 24px;
|
||||
float: left;
|
||||
background: gray;
|
||||
/* box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
|
||||
border: solid 1px rgba(255, 255, 255, .2); */
|
||||
}
|
||||
|
||||
.graph .node.Defcon1 .dot {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.graph .node.Critical .dot {
|
||||
background: #e81e1e;
|
||||
}
|
||||
|
||||
.graph .node.High .dot {
|
||||
background: #E91E63;
|
||||
}
|
||||
|
||||
.graph .node.Medium .dot {
|
||||
background: #FFA726;
|
||||
}
|
||||
|
||||
.graph .node.Low .dot {
|
||||
background: #8BC34A;
|
||||
}
|
||||
|
||||
.graph .node.Negligible .dot {
|
||||
background: #37474F;
|
||||
}
|
||||
|
||||
.graph .node.Unknown .dot {
|
||||
background: #37474F;
|
||||
}
|
||||
|
||||
.graph .node .popup {
|
||||
display: none;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -150px;
|
||||
left: 2px;
|
||||
background: white;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, .2);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
/* border: solid 1px #e2e2e2; */
|
||||
text-shadow: 0 0 0 transparent;
|
||||
}
|
||||
|
||||
.graph .node .popup:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 10px 10px 0 10px;
|
||||
border-color: white transparent transparent transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.graph .node .popup:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 11px 11px 0 10px;
|
||||
border-color: rgba(0, 0, 0, .2) transparent transparent transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.graph .node .popup > div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.graph .node:hover .dot {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.graph .node:hover .popup {
|
||||
display: block;
|
||||
max-height: 180px;
|
||||
color: dimgray;
|
||||
}
|
||||
/* bars */
|
||||
|
||||
.bar-bg {
|
||||
display: inline-block;
|
||||
width: 240px;
|
||||
height: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar-bar {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #E91E63;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.bar-bar.Defcon1 {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.bar-bar.Critical {
|
||||
background: #e81e1e;
|
||||
}
|
||||
|
||||
.bar-bar.High {
|
||||
background: #E91E63;
|
||||
}
|
||||
|
||||
.bar-bar.Medium {
|
||||
background: #FFA726;
|
||||
}
|
||||
|
||||
.bar-bar.Low {
|
||||
background: #8BC34A;
|
||||
}
|
||||
|
||||
.bar-bar.Negligible {
|
||||
background: #37474F;
|
||||
}
|
||||
|
||||
.bar-bar.Unknown {
|
||||
background: #37474F;
|
||||
}
|
||||
/* vulnerabilities */
|
||||
|
||||
.report {
|
||||
margin: 18px auto;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.report .vulnerabilities,
|
||||
.report .features > ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.report .features > ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.feature {
|
||||
border-bottom: solid 1px #ECEFF1;
|
||||
}
|
||||
|
||||
.feature:last-child {
|
||||
border-color: #CFD8DC;
|
||||
}
|
||||
|
||||
.feature__title {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.vulnerabilities {
|
||||
padding-left: 2.6em !important;
|
||||
}
|
||||
|
||||
.vulnerability {
|
||||
padding-bottom: .8em;
|
||||
padding-right: 2.2em;
|
||||
}
|
||||
|
||||
.vulnerabilities .Defcon1 .name {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.vulnerabilities .Critical .name {
|
||||
color: #e81e1e;
|
||||
}
|
||||
|
||||
.vulnerabilities .High .name {
|
||||
color: #E91E63;
|
||||
}
|
||||
|
||||
.vulnerabilities .Medium .name {
|
||||
color: #FFA726;
|
||||
}
|
||||
|
||||
.vulnerabilities .Low .name {
|
||||
color: #8BC34A;
|
||||
}
|
||||
|
||||
.vulnerabilities .Negligible .name {
|
||||
color: #37474F;
|
||||
}
|
||||
|
||||
.vulnerabilities .Unknown .name {
|
||||
color: #37474F;
|
||||
}
|
||||
/* layers */
|
||||
|
||||
.layer .layer__title {
|
||||
cursor: pointer;
|
||||
padding: 1em;
|
||||
border-bottom: solid 1px #CFD8DC;
|
||||
color: #37474F;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.layer .layer__title:hover {
|
||||
background: #ECEFF1;
|
||||
}
|
||||
|
||||
.layer.closed .features {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
display: flex;
|
||||
max-width: 940px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.summary-text .node {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-text .node:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
background: #2196F3;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.summary-text .node.Defcon1:before {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.summary-text .node.Critical:before {
|
||||
background: #e81e1e;
|
||||
}
|
||||
|
||||
.summary-text .node.High:before {
|
||||
background: #E91E63;
|
||||
}
|
||||
|
||||
.summary-text .node.Medium:before {
|
||||
background: #FFA726;
|
||||
}
|
||||
|
||||
.summary-text .node.Low:before {
|
||||
background: #8BC34A;
|
||||
}
|
||||
|
||||
.summary-text .node.Negligible:before {
|
||||
background: #37474F;
|
||||
}
|
||||
|
||||
.summary-text .node.Unknown:before {
|
||||
background: #37474F;
|
||||
}
|
||||
|
||||
.relative-graph {
|
||||
display: flex;
|
||||
max-width: 940px;
|
||||
margin: 0 auto;
|
||||
background: #2196F3;
|
||||
flex-direction: row-reverse;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.relative-graph .node {
|
||||
text-align: center;
|
||||
height: 8px;
|
||||
background: #2196F3;
|
||||
}
|
||||
|
||||
.relative-graph .node.Defcon1 {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.relative-graph .node.Critical {
|
||||
background: #e81e1e;
|
||||
}
|
||||
|
||||
.relative-graph .node.High {
|
||||
background: #E91E63;
|
||||
}
|
||||
|
||||
.relative-graph .node.Medium {
|
||||
background: #FFA726;
|
||||
}
|
||||
|
||||
.relative-graph .node.Low {
|
||||
background: #8BC34A;
|
||||
}
|
||||
|
||||
.relative-graph .node.Negligible {
|
||||
background: #37474F;
|
||||
}
|
||||
|
||||
.relative-graph .node.Unknown {
|
||||
background: #37474F;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="app-header">
|
||||
<h1>Clair Control report</h1>
|
||||
</header>
|
||||
|
||||
<div class="app-intro clearfix">
|
||||
<h2>Image: {{.ImageName}}</h2>
|
||||
{{ $ia := .}}
|
||||
|
||||
<section class="summary">
|
||||
<div>
|
||||
{{with $vulnerabilitiesCount := allVulnerabilities $ia}}
|
||||
<p><span class="lead"><strong>Total : {{$vulnerabilitiesCount.Total}} vulnerabilities</strong></span></p>
|
||||
</p>
|
||||
<div class="summary-text">
|
||||
{{if gt ($vulnerabilitiesCount.Count "Unknown") 0}}
|
||||
<div class="node Unknown">Unknown : <strong>{{$vulnerabilitiesCount.Count "Unknown"}}</strong></div>
|
||||
{{end}} {{if gt ($vulnerabilitiesCount.Count "Negligible") 0}}
|
||||
<div class="node Negligible">Negligible : <strong>{{$vulnerabilitiesCount.Count "Negligible"}}</strong></div>
|
||||
{{end}} {{if gt ($vulnerabilitiesCount.Count "Low") 0}}
|
||||
<div class="node Low">Low : <strong>{{$vulnerabilitiesCount.Count "Low"}}</strong></div>
|
||||
{{end}} {{if gt ($vulnerabilitiesCount.Count "Medium") 0}}
|
||||
<div class="node Medium">Medium : <strong>{{$vulnerabilitiesCount.Count "Medium"}}</strong></div>
|
||||
{{end}} {{if gt ($vulnerabilitiesCount.Count "High") 0}}
|
||||
<div class="node High">High : <strong>{{$vulnerabilitiesCount.Count "High"}}</strong></div>
|
||||
{{end}} {{if gt ($vulnerabilitiesCount.Count "Critical") 0}}
|
||||
<div class="node Critical">Critical : <strong>{{$vulnerabilitiesCount.Count "Critical"}}</strong></div>
|
||||
{{end}} {{if gt ($vulnerabilitiesCount.Count "Defcon1") 0}}
|
||||
<div class="node Defcon1">Defcon1 : <strong>{{$vulnerabilitiesCount.Count "Defcon1"}}</strong></div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="relative-graph">
|
||||
<div class="node Defcon1" style="width: {{$vulnerabilitiesCount.RelativeCount "Defcon1"}}%"></div>
|
||||
<div class="node Critical" style="width: {{$vulnerabilitiesCount.RelativeCount "Critical"}}%"></div>
|
||||
<div class="node High" style="width: {{$vulnerabilitiesCount.RelativeCount "High"}}%"></div>
|
||||
<div class="node Medium" style="width: {{$vulnerabilitiesCount.RelativeCount "Medium"}}%"></div>
|
||||
<div class="node Low" style="width: {{$vulnerabilitiesCount.RelativeCount "Low"}}%"></div>
|
||||
<div class="node Negligible" style="width: {{$vulnerabilitiesCount.RelativeCount "Negligible"}}%"></div>
|
||||
<div class="node Unknown" style="width: {{$vulnerabilitiesCount.RelativeCount "Unknown"}}%"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="graph">
|
||||
{{range $k,$v := vulnerabilities $ia}}
|
||||
{{range $v}}
|
||||
<a class="node {{.Severity}}" href="#{{ .Name }}">
|
||||
<div class="dot"></div>
|
||||
<div class="popup">
|
||||
<div><strong>{{.Name}}</strong></div>
|
||||
<div>{{.Severity}}</div>
|
||||
<div>{{.Feature}}</div>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="report">
|
||||
<div>
|
||||
<div class="panel">
|
||||
<div class="layers">
|
||||
<div id="{{.LastLayer.Name}}" class="layer">
|
||||
<h3 class="layer__title" data-toggle-layer="{{.LastLayer.Name}}">{{.LastLayer.Name}}</h3>
|
||||
<div class="features">
|
||||
<ul>
|
||||
{{ range sortedVulnerabilities $ia}}
|
||||
<li class="feature">
|
||||
<div class="feature__title">
|
||||
<strong>{{ .Name }}</strong> <span>{{ .Version }}</span> - <span class="fa fa-exclamation-triangle" aria-hidden="true"></span>
|
||||
</div>
|
||||
{{ range .Vulnerabilities}}
|
||||
<ul class="vulnerabilities">
|
||||
<li class="vulnerability {{ .Severity }}">
|
||||
<a class="vulnerability__title" name="{{ .Name }}"></a>
|
||||
<strong class="name">{{ .Name }}</strong>
|
||||
<div>{{ .Description }}</div>
|
||||
<a href="{{ .Link }}" target="blank">Link</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const togglers = document.querySelectorAll('[data-toggle-layer]');
|
||||
console.log(togglers);
|
||||
|
||||
for (var i = togglers.length - 1; i >= 0; i--) {
|
||||
togglers[i].onclick = function(e) {
|
||||
e.target.parentNode.classList.toggle('closed');
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
package clair
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Versions() (interface{}, error) {
|
||||
Config()
|
||||
response, err := http.Get(uri + "/versions")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("requesting Clair version: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var versionBody interface{}
|
||||
err = json.NewDecoder(response.Body).Decode(&versionBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading Clair version body: %v", err)
|
||||
}
|
||||
return versionBody, nil
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
clair:
|
||||
port: 6060
|
||||
healthPort: 6061
|
||||
uri: http://clair
|
||||
report:
|
||||
path: ./reports
|
||||
format: html
|
||||
docker:
|
||||
insecure-registries:
|
||||
- "my-own-registry:5000"
|
@ -0,0 +1,60 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const analyzeTplt = `
|
||||
Image: {{.String}}
|
||||
{{.Layers | len}} layers found
|
||||
{{$ia := .}}
|
||||
{{range .Layers}} ➜ {{with .Layer}}Analysis [{{.|$ia.ShortName}}] found {{.|$ia.CountVulnerabilities}} vulnerabilities.{{end}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var analyzeCmd = &cobra.Command{
|
||||
Use: "analyze IMAGE",
|
||||
Short: "Analyze Docker image",
|
||||
Long: `Analyze a Docker image with Clair, against Ubuntu, Red hat and Debian vulnerabilities databases`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
if len(args) != 1 {
|
||||
fmt.Printf("clairctl: \"analyze\" requires a minimum of 1 argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.ImageName = args[0]
|
||||
image, manifest, err := docker.RetrieveManifest(config.ImageName, true)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("retrieving manifest for %q: %v", config.ImageName, err)
|
||||
}
|
||||
|
||||
startLocalServer()
|
||||
if err := clair.Push(image, manifest); err != nil {
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("pushing image %q: %v", image.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
analysis := clair.Analyze(image, manifest)
|
||||
err = template.Must(template.New("analysis").Parse(analyzeTplt)).Execute(os.Stdout, analysis)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("rendering analysis: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(analyzeCmd)
|
||||
analyzeCmd.Flags().BoolVarP(&config.IsLocal, "local", "l", false, "Use local images")
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const healthTplt = `
|
||||
Clair: {{if .}}✔{{else}}✘{{end}}
|
||||
`
|
||||
|
||||
type health struct {
|
||||
Clair interface{} `json:"clair"`
|
||||
}
|
||||
|
||||
var healthCmd = &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "Get Health of clairctl and underlying services",
|
||||
Long: `Get Health of clairctl and underlying services`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ok := clair.IsHealthy()
|
||||
err := template.Must(template.New("health").Parse(healthTplt)).Execute(os.Stdout, ok)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("rendering the health: %v", err)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(healthCmd)
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const pullTplt = `
|
||||
Image: {{.Named.FullName}}:{{.Named.Tag}}
|
||||
{{.Layers | len}} layers found
|
||||
{{range .Layers}} ➜ {{.}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var pullCmd = &cobra.Command{
|
||||
Use: "pull IMAGE",
|
||||
Short: "Pull Docker image to Clair",
|
||||
Long: `Upload a Docker image to Clair for further analysis`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
if len(args) != 1 {
|
||||
fmt.Printf("clairctl: \"pull\" requires a minimum of 1 argument\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.ImageName = args[0]
|
||||
image, manifest, err := docker.RetrieveManifest(config.ImageName, true)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("retrieving manifest for %q: %v", config.ImageName, err)
|
||||
}
|
||||
|
||||
layers, err := docker.GetLayerDigests(manifest)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("retrieving layers for %q: %v", config.ImageName, err)
|
||||
}
|
||||
data := struct {
|
||||
Layers []digest.Digest
|
||||
Named reference.Named
|
||||
}{
|
||||
Layers: layers,
|
||||
Named: image,
|
||||
}
|
||||
|
||||
err = template.Must(template.New("pull").Parse(pullTplt)).Execute(os.Stdout, data)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("rendering image: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(pullCmd)
|
||||
pullCmd.Flags().BoolVarP(&config.Insecure, "insecure", "i", false, "use an insecure registry")
|
||||
pullCmd.Flags().BoolVarP(&config.IsLocal, "local", "l", false, "Use local images")
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||
"github.com/coreos/clair/cmd/clairctl/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pushCmd = &cobra.Command{
|
||||
Use: "push IMAGE",
|
||||
Short: "Push Docker image to Clair",
|
||||
Long: `Upload a Docker image to Clair for further analysis`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Printf("clairctl: \"push\" requires a minimum of 1 argument\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
startLocalServer()
|
||||
config.ImageName = args[0]
|
||||
image, manifest, err := docker.RetrieveManifest(config.ImageName, true)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("retrieving manifest for %q: %v", config.ImageName, err)
|
||||
}
|
||||
|
||||
if err := clair.Push(image, manifest); err != nil {
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("pushing image %q: %v", image.String(), err)
|
||||
}
|
||||
}
|
||||
fmt.Printf("%v has been pushed to Clair\n", image.String())
|
||||
},
|
||||
}
|
||||
|
||||
func startLocalServer() {
|
||||
sURL, err := config.LocalServerIP()
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("retrieving internal server IP: %v", err)
|
||||
}
|
||||
err = server.Serve(sURL)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("starting local server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(pushCmd)
|
||||
pushCmd.Flags().BoolVarP(&config.IsLocal, "local", "l", false, "Use local images")
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker"
|
||||
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var reportCmd = &cobra.Command{
|
||||
Use: "report IMAGE",
|
||||
Short: "Generate Docker Image vulnerabilities report",
|
||||
Long: `Generate Docker Image vulnerabilities report as HTML or JSON`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Printf("clairctl: \"report\" requires a minimum of 1 argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.ImageName = args[0]
|
||||
image, manifest, err := docker.RetrieveManifest(config.ImageName, true)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("retrieving manifest for %q: %v", config.ImageName, err)
|
||||
}
|
||||
|
||||
analyzes := clair.Analyze(image, manifest)
|
||||
imageName := strings.Replace(analyzes.ImageName, "/", "-", -1)
|
||||
if analyzes.Tag != "" {
|
||||
imageName += "-" + analyzes.Tag
|
||||
}
|
||||
|
||||
switch clair.Report.Format {
|
||||
case "html":
|
||||
html, err := clair.ReportAsHTML(analyzes)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("generating HTML report: %v", err)
|
||||
}
|
||||
err = saveReport(imageName, string(html))
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("saving HTML report: %v", err)
|
||||
}
|
||||
|
||||
case "json":
|
||||
json, err := xstrings.ToIndentJSON(analyzes)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("indenting JSON: %v", err)
|
||||
}
|
||||
err = saveReport(imageName, string(json))
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("saving JSON report: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Printf("Unsupported Report format: %v", clair.Report.Format)
|
||||
log.Fatalf("Unsupported Report format: %v", clair.Report.Format)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func saveReport(name string, content string) error {
|
||||
path := viper.GetString("clair.report.path") + "/" + clair.Report.Format
|
||||
if err := os.MkdirAll(path, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reportsName := fmt.Sprintf("%v/analysis-%v.%v", path, name, strings.ToLower(clair.Report.Format))
|
||||
f, err := os.Create(reportsName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating report file: %v", err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing report file: %v", err)
|
||||
}
|
||||
fmt.Printf("%v report at %v\n", strings.ToUpper(clair.Report.Format), reportsName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(reportCmd)
|
||||
reportCmd.Flags().BoolVarP(&config.IsLocal, "local", "l", false, "Use local images")
|
||||
reportCmd.Flags().StringP("format", "f", "html", "Format for Report [html,json]")
|
||||
viper.BindPFlag("clair.report.format", reportCmd.Flags().Lookup("format"))
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var errInternalError = errors.New("client quit unexpectedly")
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clairctl", "cmd")
|
||||
|
||||
var cfgFile string
|
||||
var logLevel string
|
||||
var noClean bool
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "clairctl",
|
||||
Short: "Analyze your docker image with Clair, directly from your registry or local images.",
|
||||
Long: ``,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := RootCmd.Execute(); err != nil {
|
||||
err = config.Clean()
|
||||
fmt.Println(err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
if err := config.Clean(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.clairctl.yml)")
|
||||
RootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "", "log level [Panic,Fatal,Error,Warn,Info,Debug]")
|
||||
RootCmd.PersistentFlags().BoolVar(&noClean, "no-clean", false, "Disable the temporary folder cleaning")
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
config.Init(cfgFile, logLevel, noClean)
|
||||
clair.Config()
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const versionTplt = `
|
||||
Clairctl version {{.}}
|
||||
`
|
||||
|
||||
var version string
|
||||
|
||||
var templ = template.Must(template.New("versions").Parse(versionTplt))
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Get Versions of Clairctl and underlying services",
|
||||
Long: `Get Versions of Clairctl and underlying services`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
err := templ.Execute(os.Stdout, version)
|
||||
if err != nil {
|
||||
fmt.Println(errInternalError)
|
||||
log.Fatalf("rendering the version: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(versionCmd)
|
||||
}
|
@ -0,0 +1,342 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/coreos/clair/cmd/clairctl/xstrings"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/jgsqware/xnet"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clairctl", "config")
|
||||
|
||||
var errNoInterfaceProvided = errors.New("could not load configuration: no interface provided")
|
||||
var errInvalidInterface = errors.New("Interface does not exist")
|
||||
var ErrLoginNotFound = errors.New("user is not log in")
|
||||
|
||||
var IsLocal = false
|
||||
var Insecure = false
|
||||
var NoClean = false
|
||||
|
||||
var ImageName string
|
||||
|
||||
type reportConfig struct {
|
||||
Path, Format string
|
||||
}
|
||||
type clairConfig struct {
|
||||
URI string
|
||||
Port, HealthPort int
|
||||
Report reportConfig
|
||||
}
|
||||
type authConfig struct {
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
type clairctlConfig struct {
|
||||
IP, Interface, TempFolder string
|
||||
Port int
|
||||
}
|
||||
type docker struct {
|
||||
InsecureRegistries []string
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Clair clairConfig
|
||||
Auth authConfig
|
||||
Clairctl clairctlConfig
|
||||
Docker docker
|
||||
}
|
||||
|
||||
// Init reads in config file and ENV variables if set.
|
||||
func Init(cfgFile string, logLevel string, noClean bool) {
|
||||
|
||||
NoClean = noClean
|
||||
|
||||
lvl := capnslog.WARNING
|
||||
if logLevel != "" {
|
||||
// Initialize logging system
|
||||
var err error
|
||||
lvl, err = capnslog.ParseLevel(strings.ToUpper(logLevel))
|
||||
if err != nil {
|
||||
log.Warningf("Wrong Log level %v, defaults to [Warning]", logLevel)
|
||||
lvl = capnslog.WARNING
|
||||
}
|
||||
|
||||
}
|
||||
capnslog.SetGlobalLogLevel(lvl)
|
||||
capnslog.SetFormatter(capnslog.NewPrettyFormatter(os.Stdout, false))
|
||||
|
||||
viper.SetEnvPrefix("clairctl")
|
||||
viper.SetConfigName("clairctl") // name of config file (without extension)
|
||||
viper.AddConfigPath("$HOME/.clairctl") // adding home directory as first search path
|
||||
viper.AddConfigPath(".") // adding home directory as first search path
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
}
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
log.Debugf("No config file used")
|
||||
} else {
|
||||
log.Debugf("Using config file: %v", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
if viper.Get("clair.uri") == nil {
|
||||
viper.Set("clair.uri", "http://localhost")
|
||||
}
|
||||
if viper.Get("clair.port") == nil {
|
||||
viper.Set("clair.port", "6060")
|
||||
}
|
||||
if viper.Get("clair.healthPort") == nil {
|
||||
viper.Set("clair.healthPort", "6061")
|
||||
}
|
||||
|
||||
if viper.Get("clair.report.path") == nil {
|
||||
viper.Set("clair.report.path", "reports")
|
||||
}
|
||||
if viper.Get("clair.report.format") == nil {
|
||||
viper.Set("clair.report.format", "html")
|
||||
}
|
||||
if viper.Get("auth.insecureSkipVerify") == nil {
|
||||
viper.Set("auth.insecureSkipVerify", "true")
|
||||
}
|
||||
if viper.Get("clairctl.ip") == nil {
|
||||
viper.Set("clairctl.ip", "")
|
||||
}
|
||||
if viper.Get("clairctl.port") == nil {
|
||||
viper.Set("clairctl.port", 0)
|
||||
}
|
||||
if viper.Get("clairctl.interface") == nil {
|
||||
viper.Set("clairctl.interface", "")
|
||||
}
|
||||
if viper.Get("clairctl.tempFolder") == nil {
|
||||
viper.Set("clairctl.tempFolder", "/tmp/clairctl")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TmpLocal() string {
|
||||
return viper.GetString("clairctl.tempFolder")
|
||||
}
|
||||
|
||||
func values() config {
|
||||
return config{
|
||||
Clair: clairConfig{
|
||||
URI: viper.GetString("clair.uri"),
|
||||
Port: viper.GetInt("clair.port"),
|
||||
HealthPort: viper.GetInt("clair.healthPort"),
|
||||
Report: reportConfig{
|
||||
Path: viper.GetString("clair.report.path"),
|
||||
Format: viper.GetString("clair.report.format"),
|
||||
},
|
||||
},
|
||||
Auth: authConfig{
|
||||
InsecureSkipVerify: viper.GetBool("auth.insecureSkipVerify"),
|
||||
},
|
||||
Clairctl: clairctlConfig{
|
||||
IP: viper.GetString("clairctl.ip"),
|
||||
Port: viper.GetInt("clairctl.port"),
|
||||
TempFolder: viper.GetString("clairctl.tempFolder"),
|
||||
Interface: viper.GetString("clairctl.interface"),
|
||||
},
|
||||
Docker: docker{
|
||||
InsecureRegistries: viper.GetStringSlice("docker.insecure-registries"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Print() {
|
||||
cfg := values()
|
||||
cfgBytes, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("marshalling configuration: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Configuration")
|
||||
fmt.Printf("%v", string(cfgBytes))
|
||||
}
|
||||
|
||||
func ClairctlHome() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p := usr.HomeDir + "/.clairctl"
|
||||
|
||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||
os.Mkdir(p, 0700)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type loginMapping map[string]Login
|
||||
|
||||
func ClairctlConfig() string {
|
||||
return ClairctlHome() + "/config.json"
|
||||
}
|
||||
|
||||
func AddLogin(registry string, login Login) error {
|
||||
var logins loginMapping
|
||||
|
||||
if err := readConfigFile(&logins, ClairctlConfig()); err != nil {
|
||||
return fmt.Errorf("reading clairctl file: %v", err)
|
||||
}
|
||||
|
||||
logins[registry] = login
|
||||
|
||||
if err := writeConfigFile(logins, ClairctlConfig()); err != nil {
|
||||
return fmt.Errorf("indenting login: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func GetLogin(registry string) (Login, error) {
|
||||
if _, err := os.Stat(ClairctlConfig()); err == nil {
|
||||
var logins loginMapping
|
||||
|
||||
if err := readConfigFile(&logins, ClairctlConfig()); err != nil {
|
||||
return Login{}, fmt.Errorf("reading clairctl file: %v", err)
|
||||
}
|
||||
|
||||
if login, present := logins[registry]; present {
|
||||
d, err := base64.StdEncoding.DecodeString(login.Password)
|
||||
if err != nil {
|
||||
return Login{}, fmt.Errorf("decoding password: %v", err)
|
||||
}
|
||||
login.Password = string(d)
|
||||
return login, nil
|
||||
}
|
||||
}
|
||||
return Login{}, ErrLoginNotFound
|
||||
}
|
||||
|
||||
func RemoveLogin(registry string) (bool, error) {
|
||||
if _, err := os.Stat(ClairctlConfig()); err == nil {
|
||||
var logins loginMapping
|
||||
|
||||
if err := readConfigFile(&logins, ClairctlConfig()); err != nil {
|
||||
return false, fmt.Errorf("reading clairctl file: %v", err)
|
||||
}
|
||||
|
||||
if _, present := logins[registry]; present {
|
||||
delete(logins, registry)
|
||||
|
||||
if err := writeConfigFile(logins, ClairctlConfig()); err != nil {
|
||||
return false, fmt.Errorf("indenting login: %v", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func readConfigFile(logins *loginMapping, file string) error {
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
f, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(f, &logins); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
*logins = loginMapping{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeConfigFile(logins loginMapping, file string) error {
|
||||
s, err := xstrings.ToIndentJSON(logins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(file, s, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//LocalServerIP return the local clairctl server IP
|
||||
func LocalServerIP() (string, error) {
|
||||
localPort := viper.GetString("clairctl.port")
|
||||
localIP := viper.GetString("clairctl.ip")
|
||||
localInterfaceConfig := viper.GetString("clairctl.interface")
|
||||
|
||||
if localIP == "" {
|
||||
log.Info("retrieving interface for local IP")
|
||||
var err error
|
||||
var localInterface net.Interface
|
||||
localInterface, err = translateInterface(localInterfaceConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("retrieving interface: %v", err)
|
||||
}
|
||||
|
||||
localIP, err = xnet.IPv4(localInterface)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("retrieving interface ip: %v", err)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(localIP) + ":" + localPort, nil
|
||||
}
|
||||
|
||||
func translateInterface(localInterface string) (net.Interface, error) {
|
||||
|
||||
if localInterface != "" {
|
||||
log.Debug("interface provided, looking for " + localInterface)
|
||||
netInterface, err := net.InterfaceByName(localInterface)
|
||||
if err != nil {
|
||||
return net.Interface{}, err
|
||||
}
|
||||
return *netInterface, nil
|
||||
}
|
||||
|
||||
log.Debug("no interface provided, looking for docker0")
|
||||
netInterface, err := net.InterfaceByName("docker0")
|
||||
if err != nil {
|
||||
log.Debug("docker0 not found, looking for first connected broadcast interface")
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return net.Interface{}, err
|
||||
}
|
||||
|
||||
i, err := xnet.First(xnet.Filter(interfaces, xnet.IsBroadcast), xnet.HasAddr)
|
||||
if err != nil {
|
||||
return net.Interface{}, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
return *netInterface, nil
|
||||
|
||||
}
|
||||
|
||||
func Clean() error {
|
||||
if IsLocal && !NoClean {
|
||||
log.Debug("cleaning temporary local repository")
|
||||
err := os.RemoveAll(TmpLocal())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("cleaning temporary local repository: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
clair:
|
||||
port: 6060
|
||||
healthPort: 6061
|
||||
uri: http://clair
|
||||
report:
|
||||
path: ./reports
|
||||
format: html
|
@ -0,0 +1,14 @@
|
||||
CONTRIBUTION
|
||||
-----------------
|
||||
|
||||
# Running full dev environnement
|
||||
|
||||
```bash
|
||||
# Running Authentication server, Registry, Clair
|
||||
docker-compose up -d
|
||||
|
||||
# Run Any command ex:
|
||||
go run main.go help
|
||||
# Or
|
||||
go run main.go pull registry:5000/wemanity-belgium/ubuntu-git
|
||||
```
|
@ -0,0 +1,29 @@
|
||||
server:
|
||||
addr: :5001
|
||||
certificate: /ssl/server.pem
|
||||
key: /ssl/server.key
|
||||
token:
|
||||
issuer: auth_service
|
||||
expiration: 900
|
||||
users:
|
||||
"": {}
|
||||
jgsqware:
|
||||
password: $2y$05$oGKwJ8QJDLBOoTBmC/EQiefIMV1N9Yt9jpX3SqMoRqZRRql6q7yym
|
||||
acl:
|
||||
- match:
|
||||
account: jgsqware
|
||||
actions: ['*']
|
||||
- match:
|
||||
account: /.+/
|
||||
name: ${account}/*
|
||||
actions: ['*']
|
||||
- match:
|
||||
type: registry
|
||||
name: catalog
|
||||
actions: ['*']
|
||||
comment: Anonymous users can get catalog.
|
||||
- match:
|
||||
account: ""
|
||||
name: /.*/
|
||||
actions: [pull]
|
||||
comment: Anonymous users can pull everything.
|
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClbJbUWDCY31g5
|
||||
hyFlhCpgXOntcqR72vVRew6vw9ooN7LKjC/ev1AbBD3JYca1R9vd/7D6wI5rYNR0
|
||||
ZtCMxaQLEgvDf166VdrZewkq8UmbVSFwqtBvcZCG7mTapzgap6jD5H/fFoFoSkFa
|
||||
V3dGWlEaS5emtk7fLjXTKO0OpByuO2mmRqAoTsnN4O3tYfrW7ReBHueibVIpjZo9
|
||||
0qyFK3sZ2ewKG98m8OGh1jzOhxM8PZDKs9KjkZ/SJiGwxIQ/Ny+rXci7q3cshQna
|
||||
n7ac2Wf0nG21j7eNk0lSKOkT3unb2+1B2+59mEtLauoYs1VcEmM55RnVdawvCmIy
|
||||
LRowuLlDAgMBAAECggEAP3ELz1gbGyXcwgNPDY3Iarh3hncHGfD5UExvb30fN3lU
|
||||
+lUVLsoUQKg5wffbqz5p1hPvndsnQ4sZL6MWrEZICW7cUBeTDsdKbUnAVFXBMu9N
|
||||
KdZ4paTaFsVqrGihHafbE3WYjMgmzQZdVfZhafvNStZezLLyQKmKPvddItZRoYfN
|
||||
sc+iFpT94hPp9Hjs9ClLQv/w9Xt8lVgD1FUh6yAlLUAn77HzbZuyC2nF4gbD2LiS
|
||||
4G+xHcH77FyAU5W6BRv1DqNsuu0ksX/93GiYx0EebzT/IXa7xc0mYE0758EXk72y
|
||||
yoznglkPkSOyyhcuI75FKMyYdQGKpyvw+y4aEv5JwQKBgQDTAaQ827Tpn/aMhP7L
|
||||
jngFgTdfeq/7Q3eZjGgtr5RFnen6YS6WzWigvh5/70ASDziFd4fyd0P41/MjPkO6
|
||||
FTFWisRCpW14+mSTUSDmgTQfsONy1Xr2ib4v4CX2NEy+nUsvpdl72dwZAG/fSu3K
|
||||
MfkVksd5Z56WJ4wxKrB4riHukQKBgQDIsren8ljtxrLepMHvaNLx5Gl1EtrgX3gy
|
||||
zTuUM2CSQktwBYNsp68AloOi6/tuH8t1xcBc8ywgCZ2WXzYdRL/koXsd2IpOTsLd
|
||||
m/zGILgRPVow70yoxKxqxW8YYuQ1gLeAOshj8IHGGfnXTvvpNQNvrnja0NzavjFU
|
||||
tR3aZQb8kwKBgQCOqNx2vQCKt7kEdmKiE1e4OQ3MAvH6SjoRWWmSAdSYYNSxkITk
|
||||
NkpX61JJouNJknrfWdpTJymQk8hx+oXlyLBL15Qrjxb9pSTcqQw6a/5msryEhisV
|
||||
hjlMuxpPZDrC4SvVMidhYgE58h6w9ELi4niKimtM/K6uzFwvXbJkVS7h0QKBgErT
|
||||
Zum0zzcHZ9TedHfACzWoRTEi8HvK3FOEdPwSE6U0FlATniY6dmKvuzBY7wrly8OD
|
||||
EO8WspLXQuu3X8OVyD2DfxVnkFkVwE1DRQDRXg7/YsrvzRL3EJlWNs9Ov2q7LK8g
|
||||
O2oXVyr2sFF33y/ZVgijceeTC2R6mIXOaOzt0acFAoGASB7aF8PTT7YzFCGK/x2O
|
||||
kg4GLJJSlDyhAZzQqe5LBZB+RhkoHZjdQHcMW84iHp8CsFqb3/D8X+5FsDkwBSMP
|
||||
bN1fCFE03BsqubtKhI9kMz5hP1OhxlMZdMxRscbdRZqo57f3imtXg6laOktYyPOy
|
||||
uOzr/Cxm5YUQqyAJ/S4zVuc=
|
||||
-----END PRIVATE KEY-----
|
@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJAOMN706JOuJOMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTYwMTI4MTcwNjE4WhcNMTcwMTI3MTcwNjE4WjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEApWyW1FgwmN9YOYchZYQqYFzp7XKke9r1UXsOr8PaKDeyyowv3r9QGwQ9
|
||||
yWHGtUfb3f+w+sCOa2DUdGbQjMWkCxILw39eulXa2XsJKvFJm1UhcKrQb3GQhu5k
|
||||
2qc4Gqeow+R/3xaBaEpBWld3RlpRGkuXprZO3y410yjtDqQcrjtppkagKE7JzeDt
|
||||
7WH61u0XgR7nom1SKY2aPdKshSt7GdnsChvfJvDhodY8zocTPD2QyrPSo5Gf0iYh
|
||||
sMSEPzcvq13Iu6t3LIUJ2p+2nNln9JxttY+3jZNJUijpE97p29vtQdvufZhLS2rq
|
||||
GLNVXBJjOeUZ1XWsLwpiMi0aMLi5QwIDAQABo1AwTjAdBgNVHQ4EFgQUWCDpNrvl
|
||||
IPntyV7Y4uyoAq+aPiQwHwYDVR0jBBgwFoAUWCDpNrvlIPntyV7Y4uyoAq+aPiQw
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAROOTzaxkx9YdvnAEj8Pt
|
||||
Ej1FgiN1ogxrL4RMwyXs5cAdHi0G71En9onB/8CaWXVrrJbu6DosNn2RQmNapxPG
|
||||
CkT7QfuYVyZF5jtigsxc+W7aLzASLZHHRA0FcgxUAlEUVaxD3xs6U2jMMntp53ij
|
||||
kOWmalMi5qOBps8PCD9sd9MDejLFihPAIz15l3TgVkbRvtcUlfmMio5AJYzjbm4/
|
||||
0c8brR9tOp3qapeT78AhOmsF7zOVygd/BRIBG+Ynzo2DudBUs/j/4VOt9D9XO4I7
|
||||
e3UaqN2OMcL5RYZ5cHemAAy9jjq9/NAYUyLLP0DiCe6OY7SKsDlGfkYVLpZMbUth
|
||||
9w==
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDP8wLYKQgqxNRw
|
||||
Liy4ZuYGHvDfK/NT0CIlvMJuobPt6NIGi6NRISLyR7eZvo6oHTN3i0ElpblzL0Hq
|
||||
JmF87d7o7tqVxO2Fq+faslXrKTgMpDiPj+WxUR3igTy2+DZ4ZP00Y4jwaPYtvwSi
|
||||
sOP2YeM6r+3sSETj5XonIr+/U3mQYkU9GgFQIbPmJnyPb+WaS0fBZ686zeIKvY3Y
|
||||
+enSCKOjss1QFyTb0TmmwQUcTBCiEXV10FSgzV383ghj+Fq9+nKo3cwooHNYwU+a
|
||||
/6gpI1GGWR074J7PGeBId4DNItcpR1x58BGmgaVHabsEW+9RdMNnh17QFk3Wt2eT
|
||||
S5Knl4UTAgMBAAECggEAcmSoZ+kKiRyGEMAV8csJNszGjL5MuQqB/mh8PQfPR00Q
|
||||
XHFsgjDMXKN/KKBfMbP+oACG8gLcpbSVeg1rC6J/QXxD2qfeUe5jOTdpdFfUcX/V
|
||||
bYQnQwfwfK3DjJO2wzwq3irzJe1Xn4q5LhZJETyAF8S4CYcn/oY6UFUZTlLJSNcH
|
||||
chQOFWvjk13DBjGAmZmjwWKxHoZsKs0ioHtShgONpPM8TZU6SmtJxdFD2pBNp+ba
|
||||
Lj5IQUYWrfCudBlzqvpXmqBlZe1J1MG+FafvAKx2CFbYkVObjRa/5DtQs99qac8y
|
||||
rhn8uloK9gljiszwwUVq/ImrUICP+20rHW+kLfHeYQKBgQD9U2XqXz0d2asD72wS
|
||||
+6yhxY4KZ3TD6W3GgfADC/kTfY+pME7KAXr//7paJJP/GtOxsLGRDHV347c3o3js
|
||||
OGlFWuUSsuJxGq4SwKuo9eRVbOMEXiVlgCuUL5HAk2co1MbKVhSJ5RGbrp6785JO
|
||||
JJcuNUTlaUsgQExEsIFJmZpbdQKBgQDSJPwl3uZIg0GC4QbQTAG1ueiQ9oPJ9kyz
|
||||
cjT31ar9L6VrLwY/MMHYKgBD5JLxkq8qL1h9ii/LJKjX7rX3yttu/qtTMO4Y82CP
|
||||
XnmR5kbODUUfiirQjTQFS3YP390nAewLwRgYPcvpyNIWA6Im6UdFJECLOTUBeiYg
|
||||
VumEhSe1ZwKBgAEj6faHHThQLYPkBQGE3n8P65bCZnUnTNYy6Yip+iILU6U4UXJ5
|
||||
VTtnxEf5mCzyyvcmy3XSr4itnrqCYt31Vwv338YYxgoqS5RMB7nH+ZIk3lS7s8Fk
|
||||
NU4CdM6AG1vEsWxhvM/uFwkzXQWNkCAH7CJKHRhHRA5OG8nHXZ2eMmKtAoGBAJ0J
|
||||
1IA8fVys8bTrkprwYcq6/ifugHfZnmHvM9QNEXWZOIXLo2BvgDyYzo/t7T2nv0zI
|
||||
Ctnt/V9SqvaKxeNB7g+ZMtC9XQC6R2t8T18PddQfqIs0RmCJVNmsFbMxOOQglJQI
|
||||
HYhoDc1MLGsVFgT8CS2LNMyV2J2c+YbrTCCjHRR7AoGAICzoSClfvjmRg+4TP9/d
|
||||
rixJF1UX77TnEhcHaFNBDnmSEX0K4rUr1o6GVZCwI+urL7ZmziDdmTDdbXWjqviJ
|
||||
73COPw798Ox50VoVWssMGZQkXfbkk2yilLbok08ohlvVhzpiyecgbxAe4C6KRWWg
|
||||
WEALyN3lILlyj1cYknRJ7gk=
|
||||
-----END PRIVATE KEY-----
|
@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDezCCAmOgAwIBAgIJAJXlshcLjIlpMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQxDTALBgNVBAMMBGF1dGgwHhcNMTYwMTI5MDcyMTE3WhcN
|
||||
MTcwMTI4MDcyMTE3WjBUMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0
|
||||
ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ0wCwYDVQQDDARh
|
||||
dXRoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz/MC2CkIKsTUcC4s
|
||||
uGbmBh7w3yvzU9AiJbzCbqGz7ejSBoujUSEi8ke3mb6OqB0zd4tBJaW5cy9B6iZh
|
||||
fO3e6O7alcTthavn2rJV6yk4DKQ4j4/lsVEd4oE8tvg2eGT9NGOI8Gj2Lb8EorDj
|
||||
9mHjOq/t7EhE4+V6JyK/v1N5kGJFPRoBUCGz5iZ8j2/lmktHwWevOs3iCr2N2Pnp
|
||||
0gijo7LNUBck29E5psEFHEwQohF1ddBUoM1d/N4IY/havfpyqN3MKKBzWMFPmv+o
|
||||
KSNRhlkdO+CezxngSHeAzSLXKUdcefARpoGlR2m7BFvvUXTDZ4de0BZN1rdnk0uS
|
||||
p5eFEwIDAQABo1AwTjAdBgNVHQ4EFgQUcCD00y15Rdvwe8VnwoZee+J+6ucwHwYD
|
||||
VR0jBBgwFoAUcCD00y15Rdvwe8VnwoZee+J+6ucwDAYDVR0TBAUwAwEB/zANBgkq
|
||||
hkiG9w0BAQsFAAOCAQEAvmlCA49FGGOZS5CWl/NzH3es3N1Gr8MihdAK0vYLxbOM
|
||||
8qA2PirEjJ6sWSeB0ZthVpk/dcod68r4dpFh7hpypvaEerFbpr+eWa9nf/KVJ/ft
|
||||
ClLw+iWZpjEjmtSbSg/XIfraWfvwQp9XNMcmIeHvovHd4HyyU1Ulx6aE31wnZ6SJ
|
||||
UKhTPgft0DRsmvFMc683jjeUg/Ik/XknnCiSyfVvwv7UEUs7sH85mE0p4giJxhEv
|
||||
7MdGlQkob+58BpzsErjoj+RpZSljna98NpwBZUfbxkYE2KzU0oqPC0zQ8KawPtw1
|
||||
OB9O45KN2mJ9dPIAbezQHolrTQ7V+49/nhTghS/T3Q==
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,77 @@
|
||||
# Copyright 2015 clair authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined.
|
||||
clair:
|
||||
database:
|
||||
# PostgreSQL Connection string
|
||||
# http://www.postgresql.org/docs/9.4/static/libpq-connect.html
|
||||
source: postgresql://postgres:5432?sslmode=disable&user=postgres&password=root
|
||||
|
||||
# Number of elements kept in the cache
|
||||
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
|
||||
cacheSize: 16384
|
||||
|
||||
api:
|
||||
# API server port
|
||||
port: 6060
|
||||
|
||||
# Health server port
|
||||
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
|
||||
healthport: 6061
|
||||
|
||||
# Deadline before an API request will respond with a 503
|
||||
timeout: 900s
|
||||
|
||||
# 32-bit URL-safe base64 key used to encrypt pagination tokens
|
||||
# If one is not provided, it will be generated.
|
||||
# Multiple clair instances in the same cluster need the same value.
|
||||
paginationKey:
|
||||
|
||||
# Optional PKI configuration
|
||||
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||
# https://github.com/coreos/etcd-ca
|
||||
# https://github.com/cloudflare/cfssl
|
||||
servername:
|
||||
cafile:
|
||||
keyfile:
|
||||
certfile:
|
||||
|
||||
updater:
|
||||
# Frequency the database will be updated with vulnerabilities from the default data sources
|
||||
# The value 0 disables the updater entirely.
|
||||
interval: 2h
|
||||
|
||||
notifier:
|
||||
# Number of attempts before the notification is marked as failed to be sent
|
||||
attempts: 3
|
||||
|
||||
# Duration before a failed notification is retried
|
||||
renotifyInterval: 2h
|
||||
|
||||
http:
|
||||
# Optional endpoint that will receive notifications via POST requests
|
||||
endpoint:
|
||||
|
||||
# Optional PKI configuration
|
||||
# If you want to easily generate client certificates and CAs, try the following projects:
|
||||
# https://github.com/cloudflare/cfssl
|
||||
# https://github.com/coreos/etcd-ca
|
||||
servername:
|
||||
cafile:
|
||||
keyfile:
|
||||
certfile:
|
||||
|
||||
# Optional HTTP Proxy: must be a valid URL (including the scheme).
|
||||
proxy:
|
@ -0,0 +1,52 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
auth:
|
||||
image: cesanta/docker_auth:stable
|
||||
ports:
|
||||
- "5001:5001"
|
||||
volumes:
|
||||
- ./auth_server/config:/config:ro
|
||||
- ./auth_server/ssl:/ssl
|
||||
command: --v=2 --alsologtostderr /config/auth_config.yml
|
||||
container_name: "auth"
|
||||
|
||||
registry:
|
||||
image: registry:2.2.1
|
||||
ports:
|
||||
- 5000:5000
|
||||
volumes:
|
||||
- ./auth_server/ssl:/ssl
|
||||
- registry-data:/var/lib/registry
|
||||
container_name: "registry"
|
||||
environment:
|
||||
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry
|
||||
# - REGISTRY_AUTH=token
|
||||
# - REGISTRY_AUTH_TOKEN_REALM=https://auth:5001/auth
|
||||
# - REGISTRY_AUTH_TOKEN_SERVICE="registry"
|
||||
# - REGISTRY_AUTH_TOKEN_ISSUER="auth_service"
|
||||
# - REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/ssl/server.pem
|
||||
|
||||
clair:
|
||||
image: quay.io/coreos/clair:v1.2.6
|
||||
volumes:
|
||||
- /tmp:/tmp
|
||||
- ./config:/config
|
||||
- clair-data:/var/local
|
||||
ports:
|
||||
- 6060:6060
|
||||
- 6061:6061
|
||||
container_name: "clair"
|
||||
command: --log-level=debug --config=/config/clair.yml
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
container_name: "postgres"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=root
|
||||
|
||||
volumes:
|
||||
clair-data:
|
||||
driver: local
|
||||
registry-data:
|
||||
driver: local
|
@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIF8DCCA9igAwIBAgIJAJ5l76MylCA7MA0GCSqGSIb3DQEBCwUAMFgxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQxETAPBgNVBAMTCHJlZ2lzdHJ5MB4XDTE3MDIyMTEwNTYy
|
||||
NVoXDTE4MDIyMTEwNTYyNVowWDELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
|
||||
U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UE
|
||||
AxMIcmVnaXN0cnkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDR+Gjf
|
||||
9eIbiFrHqBnzBFFYuG5IjlDURDY+mv+fy4NvExZZ3Zds8+KDZm9ByhZwgUJ/2BDL
|
||||
HzxtgXgqbkWGwYvCdreuFKWU1HG5V/FSxfgZDxXDaxwk8tZNq4bI39YF98F/RjFz
|
||||
+Xez1di724z89Qbxme92OzEhx3fXeDVc2cqCXYS0L8yUmR7Sy7W8LplU0kNt9SvE
|
||||
rlrrt+WpuM16B1OkxDKXdlzBkO7CTEPWWBVP7GX7rH6daXYq/I1Bo9g0daqopiNx
|
||||
h8aLoxLguu7B0Tx75sk2iCAa6s3nufnHf1jtOOeqQtS8Z8S0zu2NpcYixINK7zVF
|
||||
xIxfAxoISjrqjyZFAQ/L99781FYmChIwmnaBUoDvecQHcF95OwVSwNgetPftmeGP
|
||||
DGPxnDKpwRGr6D9n9+acoVwH7F0ICDO+baQPE5KG7jSBmtzBip21B7MtKNJkfBtI
|
||||
wvSQRQzobryMon1PpIqsy7hUwL7F4iN/WpnVvgNtfnKRZegGEaa+FDXqS/Z3gksn
|
||||
yCAr4uqb44QpsWEeGwfJZsluz4lxTjx9TXbzJR1vwDUdHmiGvu51AyfPn5N3PDgd
|
||||
N1y6I3miWfljIsPl+3wDmWrcsfxxCY/Q0C4eTFmSRgPrRhaprsEanYuq/1FswbbU
|
||||
NOMT9PgD5VahXYR4TFO3KxpAU53EUNFq1kGNYwIDAQABo4G8MIG5MB0GA1UdDgQW
|
||||
BBQhTsuacPZRW1GnXUaiCMhIkVQdijCBiQYDVR0jBIGBMH+AFCFOy5pw9lFbUadd
|
||||
RqIIyEiRVB2KoVykWjBYMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0
|
||||
ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMREwDwYDVQQDEwhy
|
||||
ZWdpc3RyeYIJAJ5l76MylCA7MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
|
||||
ggIBAJf1YpMmTnoaxiWJblvhwiDF/SJUlj1+2FC3wKWCE+EAB0UxistVaqj5tVox
|
||||
DyXeI21pwicYCUq2csLxfEbjkDwWJ4XN5Qr7Aijk31R6yv9MybblwcVXUYq1gl+n
|
||||
LCBdl0HndnFhQxxglJgidNq/K026oXWeJ/l9VJqVSSUyPvNknVpdlqIFHqaIvK3F
|
||||
q+dlzdUMsgbs6/dBFg9JRU2IIVCsGE5H66bPvLUYOxb7bwuApkG25Lr6Y9ICxSwp
|
||||
4WAp1gfIxA5DU+PWK69TJ0QYp+3vuxexMvtnNvLzZI7y/ErDT1/jTUqUan+AgMjN
|
||||
NCw2zZhGqXk80Z5/J05BstDW1XQV/xeMIPbxo4zaUKGdYBVgG+xI46AK4eUIeA6F
|
||||
GEIwI/nXcRwMk7d7kfV5UZX2w5JysmsI0onkHoHobLnEBaumYyxYPO0ddnbsbqOm
|
||||
upQnQpYmCGJudoAxbkrCAuoawL1JGar+JBQbd1yDxnWreUz4zT6akVvL3W2lcWA6
|
||||
YaBEQ5OenrHxEFmooGmTFOd1pkqmQPtuWlsm0oEQ1xzbUU5je2G5UTXk0LX8Eh55
|
||||
mue3NqzCOXqIrVZvAisOMGbQ0az+k75/8F1Votd9bF0sTaOuAqIrN7QVxWtCaVCA
|
||||
LUBdbwLC6sm3Wejojf0HLG9lTrtuRWcnrITpySA3ztcwtrHK
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKwIBAAKCAgEA0fho3/XiG4hax6gZ8wRRWLhuSI5Q1EQ2Ppr/n8uDbxMWWd2X
|
||||
bPPig2ZvQcoWcIFCf9gQyx88bYF4Km5FhsGLwna3rhSllNRxuVfxUsX4GQ8Vw2sc
|
||||
JPLWTauGyN/WBffBf0Yxc/l3s9XYu9uM/PUG8ZnvdjsxIcd313g1XNnKgl2EtC/M
|
||||
lJke0su1vC6ZVNJDbfUrxK5a67flqbjNegdTpMQyl3ZcwZDuwkxD1lgVT+xl+6x+
|
||||
nWl2KvyNQaPYNHWqqKYjcYfGi6MS4LruwdE8e+bJNoggGurN57n5x39Y7TjnqkLU
|
||||
vGfEtM7tjaXGIsSDSu81RcSMXwMaCEo66o8mRQEPy/fe/NRWJgoSMJp2gVKA73nE
|
||||
B3BfeTsFUsDYHrT37Znhjwxj8ZwyqcERq+g/Z/fmnKFcB+xdCAgzvm2kDxOShu40
|
||||
gZrcwYqdtQezLSjSZHwbSML0kEUM6G68jKJ9T6SKrMu4VMC+xeIjf1qZ1b4DbX5y
|
||||
kWXoBhGmvhQ16kv2d4JLJ8ggK+Lqm+OEKbFhHhsHyWbJbs+JcU48fU128yUdb8A1
|
||||
HR5ohr7udQMnz5+Tdzw4HTdcuiN5oln5YyLD5ft8A5lq3LH8cQmP0NAuHkxZkkYD
|
||||
60YWqa7BGp2Lqv9RbMG21DTjE/T4A+VWoV2EeExTtysaQFOdxFDRatZBjWMCAwEA
|
||||
AQKCAgEAncyH3NDoxfJa7zPplJaJIBkzYLn8CxrcfX51YD1NoNuCb7U2ST6c3E3O
|
||||
jW34IUMzm+rg7BakjlO/4HuRKu4oP9SCxIRl0I08jqOGDMQVaZfJrlzAARCzeBnR
|
||||
qQN30HJbbHBvWA6DJJcxVDVzJuRq/IXIzl071nwXF8sSp55SMFliExzdLkxJOvi3
|
||||
sx5+Q53l6SxZYW37jK1fH4dwfSYmeWyt7OCaYyquFT3Fub/m/HLYTiVb3qdUlIfL
|
||||
DSq6oOpRgH+joX39/BFpbZVvPCAoyaEvVRlGr5QJfP5qtsCBL38VtAKX6KQ/0/az
|
||||
10Ffv99aIKXXroBBUmJ9XP+UeZVtlw8ajQh8YFdArB40k92dn+skSo3lCZLoUpJt
|
||||
ZPWrwTzWlKgGfocuIcsrgRBfBu2oUJmkbG/FW9CHn+FSM8mMwAddyc48+/HyDTfb
|
||||
3Yz2719mCt7Q2TxCldTQWMqqxg6wtPEw4Z98rH6t+2dPzNBrdk/lk15Df66nrpNG
|
||||
Exhpu/AO4HWvr9DCh59jb76/IiZcJjYoi+4lzdh0ACBRj/6THV8+QE13W4jkIDDw
|
||||
xJFq9hFYqlUn1l4N6Mj8yXhYRVOlLozZe0sEkKD0U0AfZGS3BD9s044Dw4Cre7g+
|
||||
2KrCt3zJ79x1L8UjItMZsxUmeJgdbX4J922yvANn51J5hM1PbuECggEBAPiMtHGq
|
||||
2As3IPECbgoa3/dueAwmoq85AlkyB2h3GDo6seo/pkCC+6p2u/T1XmeLZX9FzQqx
|
||||
aSPJeO8it/8j1pQEdJAybFDQRQJlou3nMLa3hXll2T7A5/aP0I51emIAWybCaEX1
|
||||
zTixeZhMivAjG447IWIk3GzrWpylyndfvVWP2e/8q+bQjaPa1VQWNN/23H1N7KS5
|
||||
dRvfIpl72ZFKGnP8SVGVikpGX8AtHk9Ac3qT4xR4r/0i5AribwqAGZy1+m1AgiZW
|
||||
k103utz8bAlSo8mquUPBvLKOus6+16UbTrVDE/7Cn3vJJaDxWpLQgXmnefpntKD7
|
||||
wLKKScbHm0ECWNMCggEBANhDqLXH50n3OYmjnLFa86rARbzW7d0ZO96JEd/beNsd
|
||||
cb+36dEb/uybj9hdvARScshFjNLcj7tPSSHxlCmbvpTdxYu3pt2QOa4r/pL2RGvM
|
||||
TVDfXA+f40BIVCiMTN5eN9ErTgGc+06tgpDlTghcoZrmw7rtG9v+GmVe+akBVcqI
|
||||
mwA9yqibpxKHH5muDzwxr/Hx4dZ27jgHjOJJGjMCMIkcCboZp/CyPxX+gIRTroPs
|
||||
qog3HuY9paONVo4XXCRfSyfmoJNffOjxflgDr69PUZ+aM+VTpLN7JRUwMuwSWr4y
|
||||
BSEWn6XBkNzgo779qVlwcpwgXQr1LGtT3QhjjqufHzECggEBAJgJsgNqB1fs9BiZ
|
||||
bOh/ggsgJwz/wTpAPECFiuSLHWXZK6XoI3GI7htbICR6x7G9ImwVLZTh6ze58WEO
|
||||
stC+gm7uvsLKJVnV3LDXrS+r4S+T2XDmLVrms74uQNwz3pX+M8Pk3dYVwuBwJ7pS
|
||||
8BZu01dQsl4PwEpcOYRjIhOdm/qv0RetTxYU8t+NaDtUjimGH2AC/8PPsmRHPSn4
|
||||
CaGHW+EhLVRbjkla/Q1YTBccjMcpmZmXLchBxI8n7dbVf1VOOA8Gi9aZ1PELuyGc
|
||||
wxV82LXu2f8pjp0HFByNvum/Z4kXrC6FrPsSkxL9MHNoWhspqELVlzd2aGyOjQys
|
||||
YzsEDYMCggEBANGQxuTYQQ2Q74WsMURAAY1+YlW65KbzM+vSYarOj4+tObPxsTc8
|
||||
bMy1di/RrUd26dmeY/dVWkbFbvXglpW3YXf6a9qXbbCYePyJj1i0IdtgD7AFsb1G
|
||||
T73UGRFt23NEU8xyrVWs3G4Cf1qPig1aThO/+P2jlPKaitOetEmMjKkFtUYHmuHG
|
||||
a6DtpbaTUBohgADxRso/V1qeHmyNMEErpwLGU7qt7+qzn6RdigYw3RTj+uCioWO1
|
||||
a1RQuwZYJqbsXPTebM5CotVMZwU9FTrJnywNDqr0Yc62z1l36nCO3LYf3I6S0MOc
|
||||
Dher66FBR6Du8XDPf7oFmTSsAK2HZBJ00JECggEBAIzPXUN78o3wk+RRw6r/ioy4
|
||||
PDDiuD3fXYKBtE+/EbGK850mWqb2/1eJTBTiSTezUgc4clnDLENPXYT/3AjBavVf
|
||||
PIz9aliUzltV2qiatKWllFJoxJAE9XM81EdJUB/Z531MygjOJ6qRtxq638AeYrWa
|
||||
j2XSizhH4S0vid7dGlBPNG0uuG07pgMXvkFbkz7BEzP1VGJbjnV+Tbc455zTG8BD
|
||||
bihiGCfKMDF8ghIriBkUImSLcIG/gFE1J4/jcrZbug7duFIzws2kOZ+D32efcytG
|
||||
vFnFR+yPS5eGGtgWIc3neOiEalFG1/jhhXAnjxXoStkxXkW6yMVwdIAlOynfFtI=
|
||||
-----END RSA PRIVATE KEY-----
|
@ -0,0 +1,54 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker/dockercli"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker/dockerdist"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
//RetrieveManifest get manifest from local or remote docker registry
|
||||
func RetrieveManifest(imageName string, withExport bool) (image reference.NamedTagged, manifest distribution.Manifest, err error) {
|
||||
|
||||
if !config.IsLocal {
|
||||
image, manifest, err = dockerdist.DownloadManifest(imageName, true)
|
||||
} else {
|
||||
image, manifest, err = dockercli.GetLocalManifest(imageName, withExport)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//GetLayerDigests return layer digests from manifest schema1 and schema2
|
||||
func GetLayerDigests(manifest distribution.Manifest) ([]digest.Digest, error) {
|
||||
layers := []digest.Digest{}
|
||||
|
||||
switch manifest.(type) {
|
||||
case schema1.SignedManifest:
|
||||
for _, l := range manifest.(schema1.SignedManifest).FSLayers {
|
||||
layers = append(layers, l.BlobSum)
|
||||
}
|
||||
case *schema1.SignedManifest:
|
||||
for _, l := range manifest.(*schema1.SignedManifest).FSLayers {
|
||||
layers = append(layers, l.BlobSum)
|
||||
}
|
||||
case *schema2.DeserializedManifest:
|
||||
for _, d := range manifest.(*schema2.DeserializedManifest).Layers {
|
||||
layers = append(layers, d.Digest)
|
||||
}
|
||||
case schema2.DeserializedManifest:
|
||||
for _, d := range manifest.(schema2.DeserializedManifest).Layers {
|
||||
layers = append(layers, d.Digest)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("Not supported manifest schema type: " + reflect.TypeOf(manifest).String())
|
||||
}
|
||||
|
||||
return layers, nil
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
package dockercli
|
||||
|
||||
import (
|
||||
"compress/bzip2"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/artyom/untar"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clairctl", "dockercli")
|
||||
|
||||
//GetLocalManifest retrieve manifest for local image
|
||||
func GetLocalManifest(imageName string, withExport bool) (reference.NamedTagged, distribution.Manifest, error) {
|
||||
|
||||
n, err := reference.ParseNamed(imageName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var image reference.NamedTagged
|
||||
if reference.IsNameOnly(n) {
|
||||
image = reference.WithDefaultTag(n).(reference.NamedTagged)
|
||||
} else {
|
||||
image = n.(reference.NamedTagged)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var manifest distribution.Manifest
|
||||
if withExport {
|
||||
manifest, err = save(image.Name() + ":" + image.Tag())
|
||||
} else {
|
||||
manifest, err = historyFromCommand(image.Name() + ":" + image.Tag())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, schema1.SignedManifest{}, err
|
||||
}
|
||||
m := manifest.(schema1.SignedManifest)
|
||||
m.Name = image.Name()
|
||||
m.Tag = image.Tag()
|
||||
return image, m, err
|
||||
}
|
||||
|
||||
func saveImage(imageName string, fo *os.File) error {
|
||||
|
||||
return nil
|
||||
// save.Stderr = &stderr
|
||||
|
||||
// save.Stdout = writer
|
||||
// err := save.Run()
|
||||
// if err != nil {
|
||||
// return errors.New(stderr.String())
|
||||
// }
|
||||
|
||||
// return nil
|
||||
}
|
||||
|
||||
func save(imageName string) (distribution.Manifest, error) {
|
||||
path := config.TmpLocal() + "/" + strings.Split(imageName, ":")[0] + "/blobs"
|
||||
|
||||
if _, err := os.Stat(path); os.IsExist(err) {
|
||||
err := os.RemoveAll(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err := os.MkdirAll(path, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug("docker image to save: ", imageName)
|
||||
log.Debug("saving in: ", path)
|
||||
|
||||
cli, err := client.NewEnvClient()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
img, err := cli.ImageSave(context.Background(), []string{imageName})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
all, err := ioutil.ReadAll(img)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
img.Close()
|
||||
|
||||
fo, err := os.Create(path + "/output.tar")
|
||||
// close fo on exit and check for its returned error
|
||||
defer func() {
|
||||
if err := fo.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := fo.Write(all); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = openAndUntar(path+"/output.tar", path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = os.Remove(path + "/output.tar")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return historyFromManifest(path)
|
||||
}
|
||||
|
||||
func historyFromManifest(path string) (distribution.Manifest, error) {
|
||||
mf, err := os.Open(path + "/manifest.json")
|
||||
defer mf.Close()
|
||||
|
||||
if err != nil {
|
||||
return schema1.SignedManifest{}, err
|
||||
}
|
||||
|
||||
// https://github.com/docker/docker/blob/master/image/tarexport/tarexport.go#L17
|
||||
type manifestItem struct {
|
||||
Config string
|
||||
RepoTags []string
|
||||
Layers []string
|
||||
Parent image.ID `json:",omitempty"`
|
||||
LayerSources map[layer.DiffID]distribution.Descriptor `json:",omitempty"`
|
||||
}
|
||||
|
||||
var manifest []manifestItem
|
||||
if err = json.NewDecoder(mf).Decode(&manifest); err != nil {
|
||||
return schema1.SignedManifest{}, err
|
||||
} else if len(manifest) != 1 {
|
||||
return schema1.SignedManifest{}, err
|
||||
}
|
||||
var layers []string
|
||||
for _, layer := range manifest[0].Layers {
|
||||
layers = append(layers, strings.TrimSuffix(layer, "/layer.tar"))
|
||||
}
|
||||
var m schema1.SignedManifest
|
||||
|
||||
for _, layer := range manifest[0].Layers {
|
||||
var d digest.Digest
|
||||
d, err := digest.Parse("sha256:" + strings.TrimSuffix(layer, "/layer.tar"))
|
||||
if err != nil {
|
||||
return schema1.SignedManifest{}, err
|
||||
}
|
||||
m.FSLayers = append(m.FSLayers, schema1.FSLayer{BlobSum: d})
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func historyFromCommand(imageName string) (schema1.SignedManifest, error) {
|
||||
|
||||
client, err := client.NewEnvClient()
|
||||
if err != nil {
|
||||
return schema1.SignedManifest{}, err
|
||||
}
|
||||
histories, err := client.ImageHistory(context.Background(), imageName)
|
||||
|
||||
manifest := schema1.SignedManifest{}
|
||||
for _, history := range histories {
|
||||
var d digest.Digest
|
||||
d, err := digest.Parse(history.ID)
|
||||
if err != nil {
|
||||
return schema1.SignedManifest{}, err
|
||||
}
|
||||
manifest.FSLayers = append(manifest.FSLayers, schema1.FSLayer{BlobSum: d})
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func openAndUntar(name, dst string) error {
|
||||
var rd io.Reader
|
||||
f, err := os.Open(name)
|
||||
defer f.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rd = f
|
||||
if strings.HasSuffix(name, ".gz") || strings.HasSuffix(name, ".tgz") {
|
||||
gr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gr.Close()
|
||||
rd = gr
|
||||
} else if strings.HasSuffix(name, ".bz2") {
|
||||
rd = bzip2.NewReader(f)
|
||||
}
|
||||
if err := os.MkdirAll(dst, os.ModeDir|os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
// resetting umask is essential to have exact permissions on unpacked
|
||||
// files; it's not not put inside untar function as it changes
|
||||
// process-wide umask
|
||||
mask := syscall.Umask(0)
|
||||
defer syscall.Umask(mask)
|
||||
return untar.Untar(rd, dst)
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package dockerdist
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
)
|
||||
|
||||
//ErrUnauthorized is return when requested user don't have access to the resource
|
||||
var ErrUnauthorized = errors.New("unauthorized access")
|
||||
|
||||
//bearerAuthParams parse Bearer Token on Www-Authenticate header
|
||||
func bearerAuthParams(r *http.Response) map[string]string {
|
||||
s := strings.SplitN(r.Header.Get("Www-Authenticate"), " ", 2)
|
||||
if len(s) != 2 || s[0] != "Bearer" {
|
||||
return nil
|
||||
}
|
||||
result := map[string]string{}
|
||||
|
||||
for _, kv := range strings.Split(s[1], ",") {
|
||||
parts := strings.Split(kv, "=")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
result[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
//AuthenticateResponse add authentication headers on request
|
||||
func AuthenticateResponse(client *http.Client, dockerResponse *http.Response, request *http.Request) error {
|
||||
bearerToken := bearerAuthParams(dockerResponse)
|
||||
url := bearerToken["realm"] + "?service=" + url.QueryEscape(bearerToken["service"])
|
||||
if bearerToken["scope"] != "" {
|
||||
url += "&scope=" + bearerToken["scope"]
|
||||
}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authConfig, err := GetAuthCredentials(config.ImageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(authConfig.Username, authConfig.Password)
|
||||
|
||||
response, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode == http.StatusUnauthorized {
|
||||
return ErrUnauthorized
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("authentication server response: %v - %v", response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
type token struct {
|
||||
Value string `json:"token"`
|
||||
}
|
||||
var tok token
|
||||
err = json.NewDecoder(response.Body).Decode(&tok)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", "Bearer "+tok.Value)
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
// Copyright 2016 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package dockerdist provides helper methods for retrieving and parsing a
|
||||
// information from a remote Docker repository.
|
||||
package dockerdist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
distlib "github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli/config"
|
||||
"github.com/docker/docker/distribution"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/jgsqware/clairctl", "dockerdist")
|
||||
|
||||
var ErrTagNotFound = errors.New("this image or tag is not found")
|
||||
|
||||
func isInsecureRegistry(registryHostname string) bool {
|
||||
for _, r := range viper.GetStringSlice("docker.insecure-registries") {
|
||||
if r == registryHostname {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getService() *registry.DefaultService {
|
||||
serviceOptions := registry.ServiceOptions{
|
||||
InsecureRegistries: viper.GetStringSlice("docker.insecure-registries"),
|
||||
}
|
||||
return registry.NewService(serviceOptions)
|
||||
}
|
||||
|
||||
// getRepositoryClient returns a client for performing registry operations against the given named
|
||||
// image.
|
||||
func getRepositoryClient(image reference.Named, insecure bool, scopes ...string) (distlib.Repository, error) {
|
||||
service := getService()
|
||||
log.Debugf("Retrieving repository client")
|
||||
|
||||
ctx := context.Background()
|
||||
authConfig, err := GetAuthCredentials(image.String())
|
||||
if err != nil {
|
||||
log.Debugf("GetAuthCredentials error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if (types.AuthConfig{}) != authConfig {
|
||||
|
||||
userAgent := dockerversion.DockerUserAgent(ctx)
|
||||
_, _, err = service.Auth(ctx, &authConfig, userAgent)
|
||||
if err != nil {
|
||||
log.Debugf("Auth: err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
repoInfo, err := service.ResolveRepository(image)
|
||||
if err != nil {
|
||||
log.Debugf("ResolveRepository err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaHeaders := map[string][]string{}
|
||||
|
||||
endpoints, err := service.LookupPullEndpoints(image.Hostname())
|
||||
if err != nil {
|
||||
log.Debugf("registry.LookupPullEndpoints error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var confirmedV2 bool
|
||||
var repository distlib.Repository
|
||||
for _, endpoint := range endpoints {
|
||||
if confirmedV2 && endpoint.Version == registry.APIVersion1 {
|
||||
log.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
|
||||
continue
|
||||
}
|
||||
|
||||
endpoint.TLSConfig.InsecureSkipVerify = viper.GetBool("auth.insecureSkipVerify")
|
||||
if isInsecureRegistry(endpoint.URL.Host) {
|
||||
endpoint.URL.Scheme = "http"
|
||||
}
|
||||
log.Debugf("endpoint.TLSConfig.InsecureSkipVerify: %v", endpoint.TLSConfig.InsecureSkipVerify)
|
||||
repository, confirmedV2, err = distribution.NewV2Repository(ctx, repoInfo, endpoint, metaHeaders, &authConfig, scopes...)
|
||||
if err != nil {
|
||||
log.Debugf("cannot instanciate new v2 repository on %v", endpoint.URL)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !confirmedV2 {
|
||||
return nil, errors.New("Only V2 repository are supported")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return repository, nil
|
||||
}
|
||||
|
||||
func GetPushURL(hostname string) (*url.URL, error) {
|
||||
service := getService()
|
||||
endpoints, err := service.LookupPushEndpoints(hostname)
|
||||
if err != nil {
|
||||
log.Debugf("registry.LookupPushEndpoints error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint.TLSConfig.InsecureSkipVerify = viper.GetBool("auth.insecureSkipVerify")
|
||||
if isInsecureRegistry(endpoint.URL.Host) {
|
||||
endpoint.URL.Scheme = "http"
|
||||
}
|
||||
return endpoint.URL, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("No endpoints found")
|
||||
}
|
||||
|
||||
// getDigest returns the digest for the given image.
|
||||
func getDigest(ctx context.Context, repo distlib.Repository, image reference.Named) (digest.Digest, error) {
|
||||
if withDigest, ok := image.(reference.Canonical); ok {
|
||||
return withDigest.Digest(), nil
|
||||
}
|
||||
// Get TagService.
|
||||
tagSvc := repo.Tags(ctx)
|
||||
|
||||
// Get Tag name.
|
||||
tag := "latest"
|
||||
if withTag, ok := image.(reference.NamedTagged); ok {
|
||||
tag = withTag.Tag()
|
||||
}
|
||||
|
||||
// Get Tag's Descriptor.
|
||||
descriptor, err := tagSvc.Get(ctx, tag)
|
||||
if err != nil {
|
||||
|
||||
// Docker returns an UnexpectedHTTPResponseError if it cannot parse the JSON body of an
|
||||
// unexpected error. Unfortunately, HEAD requests *by definition* don't have bodies, so
|
||||
// Docker will return this error for non-200 HEAD requests. We therefore have to hack
|
||||
// around it... *sigh*.
|
||||
if _, ok := err.(*client.UnexpectedHTTPResponseError); ok {
|
||||
return "", errors.New("Received error when trying to fetch the specified tag: it might not exist or you do not have access")
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), v2.ErrorCodeManifestUnknown.Message()) {
|
||||
return "", ErrTagNotFound
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return descriptor.Digest, nil
|
||||
}
|
||||
|
||||
// GetAuthCredentials returns the auth credentials (if any found) for the given repository, as found
|
||||
// in the user's docker config.
|
||||
func GetAuthCredentials(image string) (types.AuthConfig, error) {
|
||||
// Lookup the index information for the name.
|
||||
indexInfo, err := registry.ParseSearchIndexInfo(image)
|
||||
if err != nil {
|
||||
return types.AuthConfig{}, err
|
||||
}
|
||||
// Retrieve the user's Docker configuration file (if any).
|
||||
configFile, err := config.Load(config.Dir())
|
||||
if err != nil {
|
||||
return types.AuthConfig{}, err
|
||||
}
|
||||
|
||||
// Resolve the authentication information for the registry specified, via the config file.
|
||||
return registry.ResolveAuthConfig(configFile.AuthConfigs, indexInfo), nil
|
||||
}
|
||||
|
||||
// DownloadManifest the manifest for the given image, using the given credentials.
|
||||
func DownloadManifest(image string, insecure bool) (reference.NamedTagged, distlib.Manifest, error) {
|
||||
log.Debugf("Downloading manifest for %v", image)
|
||||
// Parse the image name as a docker image reference.
|
||||
n, err := reference.ParseNamed(image)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if reference.IsNameOnly(n) {
|
||||
n, _ = reference.ParseNamed(image + ":" + reference.DefaultTag)
|
||||
}
|
||||
|
||||
named := n.(reference.NamedTagged)
|
||||
|
||||
// Create a reference to a repository client for the repo.
|
||||
repo, err := getRepositoryClient(named, insecure, "pull")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Get the digest.
|
||||
ctx := context.Background()
|
||||
|
||||
digest, err := getDigest(ctx, repo, named)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Retrieve the manifest for the tag.
|
||||
manSvc, err := repo.Manifests(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
manifest, err := manSvc.Get(ctx, digest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Verify the manifest if it's signed.
|
||||
log.Debugf("manifest type: %v", reflect.TypeOf(manifest))
|
||||
|
||||
switch manifest.(type) {
|
||||
case *schema1.SignedManifest:
|
||||
_, verr := schema1.Verify(manifest.(*schema1.SignedManifest))
|
||||
if verr != nil {
|
||||
return nil, nil, verr
|
||||
}
|
||||
case *schema2.DeserializedManifest:
|
||||
log.Debugf("retrieved schema2 manifest, no verification")
|
||||
default:
|
||||
log.Printf("Could not verify manifest for image %v: not signed", image)
|
||||
}
|
||||
|
||||
return named, manifest, nil
|
||||
}
|
||||
|
||||
// DownloadV1Manifest the manifest for the given image in v1 schema format, using the given credentials.
|
||||
func DownloadV1Manifest(imageName string, insecure bool) (reference.NamedTagged, schema1.SignedManifest, error) {
|
||||
image, manifest, err := DownloadManifest(imageName, insecure)
|
||||
|
||||
if err != nil {
|
||||
return nil, schema1.SignedManifest{}, err
|
||||
}
|
||||
// Ensure that the manifest type is supported.
|
||||
switch manifest.(type) {
|
||||
case *schema1.SignedManifest:
|
||||
return image, *manifest.(*schema1.SignedManifest), nil
|
||||
default:
|
||||
return nil, schema1.SignedManifest{}, errors.New("only v1 manifests are currently supported")
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// Copyright © 2016 NAME HERE <EMAIL ADDRESS>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/coreos/clair/cmd/clairctl/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/coreos/clair/cmd/clairctl/clair"
|
||||
"github.com/coreos/clair/cmd/clairctl/config"
|
||||
"github.com/coreos/clair/cmd/clairctl/docker/dockerdist"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clairctl", "server")
|
||||
|
||||
//Serve run a local server with the fileserver and the reverse proxy
|
||||
func Serve(sURL string) error {
|
||||
|
||||
go func() {
|
||||
http.Handle("/v2/", newSingleHostReverseProxy())
|
||||
http.Handle("/local/", http.StripPrefix("/local", restrictedFileServer(config.TmpLocal())))
|
||||
|
||||
listener := tcpListener(sURL)
|
||||
log.Info("Starting Server on ", listener.Addr())
|
||||
|
||||
if err := http.Serve(listener, nil); err != nil {
|
||||
log.Fatalf("local server error: %v", err)
|
||||
}
|
||||
}()
|
||||
//sleep needed to wait the server start. Maybe use a channel for that
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
|
||||
func tcpListener(sURL string) (listener net.Listener) {
|
||||
listener, err := net.Listen("tcp", sURL)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot instanciate listener: %v", err)
|
||||
}
|
||||
|
||||
if viper.GetInt("clairctl.port") == 0 {
|
||||
port := strings.Split(listener.Addr().String(), ":")[1]
|
||||
log.Debugf("Update local server port from %q to %q", "0", port)
|
||||
viper.Set("clairctl.port", port)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func restrictedFileServer(path string) http.Handler {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
os.Mkdir(path, 0777)
|
||||
}
|
||||
|
||||
fc := func(w http.ResponseWriter, r *http.Request) {
|
||||
http.FileServer(http.Dir(path)).ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fc)
|
||||
}
|
||||
|
||||
func newSingleHostReverseProxy() *httputil.ReverseProxy {
|
||||
director := func(request *http.Request) {
|
||||
|
||||
var validID = regexp.MustCompile(`.*/blobs/(.*)$`)
|
||||
u := request.URL.Path
|
||||
log.Debugf("request url: %v", u)
|
||||
log.Debugf("request for image: %v", config.ImageName)
|
||||
if !validID.MatchString(u) {
|
||||
log.Errorf("cannot parse url: %v", u)
|
||||
}
|
||||
var host string
|
||||
host, err := clair.GetRegistryMapping(validID.FindStringSubmatch(u)[1])
|
||||
log.Debugf("host retreived: %v", host)
|
||||
if err != nil {
|
||||
log.Errorf("response error: %v", err)
|
||||
return
|
||||
}
|
||||
out, _ := url.Parse(host)
|
||||
request.URL.Scheme = out.Scheme
|
||||
request.URL.Host = out.Host
|
||||
client := &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: viper.GetBool("auth.insecureSkipVerify")},
|
||||
DisableCompression: true,
|
||||
}}
|
||||
|
||||
log.Debugf("auth.insecureSkipVerify: %v", viper.GetBool("auth.insecureSkipVerify"))
|
||||
log.Debugf("request.URL.String(): %v", request.URL.String())
|
||||
req, _ := http.NewRequest("HEAD", request.URL.String(), nil)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Errorf("response error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
log.Info("pull from clair is unauthorized")
|
||||
dockerdist.AuthenticateResponse(client, resp, request)
|
||||
}
|
||||
|
||||
r, _ := http.NewRequest("GET", request.URL.String(), nil)
|
||||
r.Header.Set("Authorization", request.Header.Get("Authorization"))
|
||||
r.Header.Set("Accept-Encoding", request.Header.Get("Accept-Encoding"))
|
||||
*request = *r
|
||||
}
|
||||
return &httputil.ReverseProxy{
|
||||
Director: director,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: viper.GetBool("auth.insecureSkipVerify")},
|
||||
DisableCompression: true,
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/clair/cmd/clairctl", "test")
|
||||
|
||||
func CreateTmpConfigFile(content string) string {
|
||||
|
||||
c := []byte(content)
|
||||
tmpfile, err := ioutil.TempFile("", "test-clairctl")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if content != "" {
|
||||
if _, err := tmpfile.Write(c); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(tmpfile.Name()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return tmpfile.Name()
|
||||
}
|
||||
|
||||
func CreateConfigFile(content string, name string, path string) string {
|
||||
if err := ioutil.WriteFile(path+"/"+name, []byte(content), 0600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return path + "/" + name
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package xstrings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//Substr extract string of length in s starting at pos
|
||||
func Substr(s string, pos, length int) string {
|
||||
runes := []rune(s)
|
||||
l := pos + length
|
||||
if l > len(runes) {
|
||||
l = len(runes)
|
||||
}
|
||||
return string(runes[pos:l])
|
||||
}
|
||||
|
||||
//TrimPrefixSuffix combine TrimPrefix and TrimSuffix
|
||||
func TrimPrefixSuffix(s string, prefix string, suffix string) string {
|
||||
return strings.TrimSuffix(strings.TrimPrefix(s, prefix), suffix)
|
||||
}
|
||||
|
||||
func ToIndentJSON(v interface{}) ([]byte, error) {
|
||||
b, err := json.MarshalIndent(v, "", "\t")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package xstrings
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSubstrFromBeginning(t *testing.T) {
|
||||
commitID := "e3ff9321271b0a5cec45ca6e0cdc72b2f376afd2"
|
||||
expected := "e3ff9"
|
||||
if s := Substr(commitID, 0, 5); s != expected {
|
||||
t.Errorf("is %v, expect %v", s, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubstrFromCharFive(t *testing.T) {
|
||||
commitID := "e3ff9321271b0a5cec45ca6e0cdc72b2f376afd2"
|
||||
expected := "32127"
|
||||
if s := Substr(commitID, 5, 5); s != expected {
|
||||
t.Errorf("is %v, expect %v", s, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimPrefixSuffix(t *testing.T) {
|
||||
v := "http://registry:5555/v2"
|
||||
e := "registry:5555"
|
||||
if s := TrimPrefixSuffix(v, "http://", "/v2"); s != e {
|
||||
t.Errorf("is %v, expect %v", s, e)
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,12 @@
|
||||
# go-ansiterm
|
||||
|
||||
This is a cross platform Ansi Terminal Emulation library. It reads a stream of Ansi characters and produces the appropriate function calls. The results of the function calls are platform dependent.
|
||||
|
||||
For example the parser might receive "ESC, [, A" as a stream of three characters. This is the code for Cursor Up (http://www.vt100.net/docs/vt510-rm/CUU). The parser then calls the cursor up function (CUU()) on an event handler. The event handler determines what platform specific work must be done to cause the cursor to move up one position.
|
||||
|
||||
The parser (parser.go) is a partial implementation of this state machine (http://vt100.net/emu/vt500_parser.png). There are also two event handler implementations, one for tests (test_event_handler.go) to validate that the expected events are being produced and called, the other is a Windows implementation (winterm/win_event_handler.go).
|
||||
|
||||
See parser_test.go for examples exercising the state machine and generating appropriate function calls.
|
||||
|
||||
-----
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
@ -0,0 +1,188 @@
|
||||
package ansiterm
|
||||
|
||||
const LogEnv = "DEBUG_TERMINAL"
|
||||
|
||||
// ANSI constants
|
||||
// References:
|
||||
// -- http://www.ecma-international.org/publications/standards/Ecma-048.htm
|
||||
// -- http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||
// -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||
// -- http://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
// -- http://vt100.net/emu/dec_ansi_parser
|
||||
// -- http://vt100.net/emu/vt500_parser.svg
|
||||
// -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
// -- http://www.inwap.com/pdp10/ansicode.txt
|
||||
const (
|
||||
// ECMA-48 Set Graphics Rendition
|
||||
// Note:
|
||||
// -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved
|
||||
// -- Fonts could possibly be supported via SetCurrentConsoleFontEx
|
||||
// -- Windows does not expose the per-window cursor (i.e., caret) blink times
|
||||
ANSI_SGR_RESET = 0
|
||||
ANSI_SGR_BOLD = 1
|
||||
ANSI_SGR_DIM = 2
|
||||
_ANSI_SGR_ITALIC = 3
|
||||
ANSI_SGR_UNDERLINE = 4
|
||||
_ANSI_SGR_BLINKSLOW = 5
|
||||
_ANSI_SGR_BLINKFAST = 6
|
||||
ANSI_SGR_REVERSE = 7
|
||||
_ANSI_SGR_INVISIBLE = 8
|
||||
_ANSI_SGR_LINETHROUGH = 9
|
||||
_ANSI_SGR_FONT_00 = 10
|
||||
_ANSI_SGR_FONT_01 = 11
|
||||
_ANSI_SGR_FONT_02 = 12
|
||||
_ANSI_SGR_FONT_03 = 13
|
||||
_ANSI_SGR_FONT_04 = 14
|
||||
_ANSI_SGR_FONT_05 = 15
|
||||
_ANSI_SGR_FONT_06 = 16
|
||||
_ANSI_SGR_FONT_07 = 17
|
||||
_ANSI_SGR_FONT_08 = 18
|
||||
_ANSI_SGR_FONT_09 = 19
|
||||
_ANSI_SGR_FONT_10 = 20
|
||||
_ANSI_SGR_DOUBLEUNDERLINE = 21
|
||||
ANSI_SGR_BOLD_DIM_OFF = 22
|
||||
_ANSI_SGR_ITALIC_OFF = 23
|
||||
ANSI_SGR_UNDERLINE_OFF = 24
|
||||
_ANSI_SGR_BLINK_OFF = 25
|
||||
_ANSI_SGR_RESERVED_00 = 26
|
||||
ANSI_SGR_REVERSE_OFF = 27
|
||||
_ANSI_SGR_INVISIBLE_OFF = 28
|
||||
_ANSI_SGR_LINETHROUGH_OFF = 29
|
||||
ANSI_SGR_FOREGROUND_BLACK = 30
|
||||
ANSI_SGR_FOREGROUND_RED = 31
|
||||
ANSI_SGR_FOREGROUND_GREEN = 32
|
||||
ANSI_SGR_FOREGROUND_YELLOW = 33
|
||||
ANSI_SGR_FOREGROUND_BLUE = 34
|
||||
ANSI_SGR_FOREGROUND_MAGENTA = 35
|
||||
ANSI_SGR_FOREGROUND_CYAN = 36
|
||||
ANSI_SGR_FOREGROUND_WHITE = 37
|
||||
_ANSI_SGR_RESERVED_01 = 38
|
||||
ANSI_SGR_FOREGROUND_DEFAULT = 39
|
||||
ANSI_SGR_BACKGROUND_BLACK = 40
|
||||
ANSI_SGR_BACKGROUND_RED = 41
|
||||
ANSI_SGR_BACKGROUND_GREEN = 42
|
||||
ANSI_SGR_BACKGROUND_YELLOW = 43
|
||||
ANSI_SGR_BACKGROUND_BLUE = 44
|
||||
ANSI_SGR_BACKGROUND_MAGENTA = 45
|
||||
ANSI_SGR_BACKGROUND_CYAN = 46
|
||||
ANSI_SGR_BACKGROUND_WHITE = 47
|
||||
_ANSI_SGR_RESERVED_02 = 48
|
||||
ANSI_SGR_BACKGROUND_DEFAULT = 49
|
||||
// 50 - 65: Unsupported
|
||||
|
||||
ANSI_MAX_CMD_LENGTH = 4096
|
||||
|
||||
MAX_INPUT_EVENTS = 128
|
||||
DEFAULT_WIDTH = 80
|
||||
DEFAULT_HEIGHT = 24
|
||||
|
||||
ANSI_BEL = 0x07
|
||||
ANSI_BACKSPACE = 0x08
|
||||
ANSI_TAB = 0x09
|
||||
ANSI_LINE_FEED = 0x0A
|
||||
ANSI_VERTICAL_TAB = 0x0B
|
||||
ANSI_FORM_FEED = 0x0C
|
||||
ANSI_CARRIAGE_RETURN = 0x0D
|
||||
ANSI_ESCAPE_PRIMARY = 0x1B
|
||||
ANSI_ESCAPE_SECONDARY = 0x5B
|
||||
ANSI_OSC_STRING_ENTRY = 0x5D
|
||||
ANSI_COMMAND_FIRST = 0x40
|
||||
ANSI_COMMAND_LAST = 0x7E
|
||||
DCS_ENTRY = 0x90
|
||||
CSI_ENTRY = 0x9B
|
||||
OSC_STRING = 0x9D
|
||||
ANSI_PARAMETER_SEP = ";"
|
||||
ANSI_CMD_G0 = '('
|
||||
ANSI_CMD_G1 = ')'
|
||||
ANSI_CMD_G2 = '*'
|
||||
ANSI_CMD_G3 = '+'
|
||||
ANSI_CMD_DECPNM = '>'
|
||||
ANSI_CMD_DECPAM = '='
|
||||
ANSI_CMD_OSC = ']'
|
||||
ANSI_CMD_STR_TERM = '\\'
|
||||
|
||||
KEY_CONTROL_PARAM_2 = ";2"
|
||||
KEY_CONTROL_PARAM_3 = ";3"
|
||||
KEY_CONTROL_PARAM_4 = ";4"
|
||||
KEY_CONTROL_PARAM_5 = ";5"
|
||||
KEY_CONTROL_PARAM_6 = ";6"
|
||||
KEY_CONTROL_PARAM_7 = ";7"
|
||||
KEY_CONTROL_PARAM_8 = ";8"
|
||||
KEY_ESC_CSI = "\x1B["
|
||||
KEY_ESC_N = "\x1BN"
|
||||
KEY_ESC_O = "\x1BO"
|
||||
|
||||
FILL_CHARACTER = ' '
|
||||
)
|
||||
|
||||
func getByteRange(start byte, end byte) []byte {
|
||||
bytes := make([]byte, 0, 32)
|
||||
for i := start; i <= end; i++ {
|
||||
bytes = append(bytes, byte(i))
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
var toGroundBytes = getToGroundBytes()
|
||||
var executors = getExecuteBytes()
|
||||
|
||||
// SPACE 20+A0 hex Always and everywhere a blank space
|
||||
// Intermediate 20-2F hex !"#$%&'()*+,-./
|
||||
var intermeds = getByteRange(0x20, 0x2F)
|
||||
|
||||
// Parameters 30-3F hex 0123456789:;<=>?
|
||||
// CSI Parameters 30-39, 3B hex 0123456789;
|
||||
var csiParams = getByteRange(0x30, 0x3F)
|
||||
|
||||
var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...)
|
||||
|
||||
// Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||
var upperCase = getByteRange(0x40, 0x5F)
|
||||
|
||||
// Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
|
||||
var lowerCase = getByteRange(0x60, 0x7E)
|
||||
|
||||
// Alphabetics 40-7E hex (all of upper and lower case)
|
||||
var alphabetics = append(upperCase, lowerCase...)
|
||||
|
||||
var printables = getByteRange(0x20, 0x7F)
|
||||
|
||||
var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E)
|
||||
var escapeToGroundBytes = getEscapeToGroundBytes()
|
||||
|
||||
// See http://www.vt100.net/emu/vt500_parser.png for description of the complex
|
||||
// byte ranges below
|
||||
|
||||
func getEscapeToGroundBytes() []byte {
|
||||
escapeToGroundBytes := getByteRange(0x30, 0x4F)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, 0x59)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, 0x5A)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, 0x5C)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...)
|
||||
return escapeToGroundBytes
|
||||
}
|
||||
|
||||
func getExecuteBytes() []byte {
|
||||
executeBytes := getByteRange(0x00, 0x17)
|
||||
executeBytes = append(executeBytes, 0x19)
|
||||
executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...)
|
||||
return executeBytes
|
||||
}
|
||||
|
||||
func getToGroundBytes() []byte {
|
||||
groundBytes := []byte{0x18}
|
||||
groundBytes = append(groundBytes, 0x1A)
|
||||
groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...)
|
||||
groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...)
|
||||
groundBytes = append(groundBytes, 0x99)
|
||||
groundBytes = append(groundBytes, 0x9A)
|
||||
groundBytes = append(groundBytes, 0x9C)
|
||||
return groundBytes
|
||||
}
|
||||
|
||||
// Delete 7F hex Always and everywhere ignored
|
||||
// C1 Control 80-9F hex 32 additional control characters
|
||||
// G1 Displayable A1-FE hex 94 additional displayable characters
|
||||
// Special A0+FF hex Same as SPACE and DELETE
|
@ -0,0 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
type ansiContext struct {
|
||||
currentChar byte
|
||||
paramBuffer []byte
|
||||
interBuffer []byte
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package ansiterm
|
||||
|
||||
type csiEntryState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (csiState csiEntryState) Handle(b byte) (s state, e error) {
|
||||
logger.Infof("CsiEntry::Handle %#x", b)
|
||||
|
||||
nextState, err := csiState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
return csiState.parser.csiParam, nil
|
||||
case sliceContains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
|
||||
return csiState, nil
|
||||
}
|
||||
|
||||
func (csiState csiEntryState) Transition(s state) error {
|
||||
logger.Infof("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
return csiState.parser.csiDispatch()
|
||||
case csiState.parser.csiParam:
|
||||
switch {
|
||||
case sliceContains(csiParams, csiState.parser.context.currentChar):
|
||||
csiState.parser.collectParam()
|
||||
case sliceContains(intermeds, csiState.parser.context.currentChar):
|
||||
csiState.parser.collectInter()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (csiState csiEntryState) Enter() error {
|
||||
csiState.parser.clear()
|
||||
return nil
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package ansiterm
|
||||
|
||||
type csiParamState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||
logger.Infof("CsiParam::Handle %#x", b)
|
||||
|
||||
nextState, err := csiState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
csiState.parser.collectParam()
|
||||
return csiState, nil
|
||||
case sliceContains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
|
||||
return csiState, nil
|
||||
}
|
||||
|
||||
func (csiState csiParamState) Transition(s state) error {
|
||||
logger.Infof("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
return csiState.parser.csiDispatch()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package ansiterm
|
||||
|
||||
type escapeIntermediateState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
|
||||
logger.Infof("escapeIntermediateState::Handle %#x", b)
|
||||
nextState, err := escState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(intermeds, b):
|
||||
return escState, escState.parser.collectInter()
|
||||
case sliceContains(executors, b):
|
||||
return escState, escState.parser.execute()
|
||||
case sliceContains(escapeIntermediateToGroundBytes, b):
|
||||
return escState.parser.ground, nil
|
||||
}
|
||||
|
||||
return escState, nil
|
||||
}
|
||||
|
||||
func (escState escapeIntermediateState) Transition(s state) error {
|
||||
logger.Infof("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
|
||||
escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
return escState.parser.escDispatch()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package ansiterm
|
||||
|
||||
type escapeState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (escState escapeState) Handle(b byte) (s state, e error) {
|
||||
logger.Infof("escapeState::Handle %#x", b)
|
||||
nextState, err := escState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case b == ANSI_ESCAPE_SECONDARY:
|
||||
return escState.parser.csiEntry, nil
|
||||
case b == ANSI_OSC_STRING_ENTRY:
|
||||
return escState.parser.oscString, nil
|
||||
case sliceContains(executors, b):
|
||||
return escState, escState.parser.execute()
|
||||
case sliceContains(escapeToGroundBytes, b):
|
||||
return escState.parser.ground, nil
|
||||
case sliceContains(intermeds, b):
|
||||
return escState.parser.escapeIntermediate, nil
|
||||
}
|
||||
|
||||
return escState, nil
|
||||
}
|
||||
|
||||
func (escState escapeState) Transition(s state) error {
|
||||
logger.Infof("Escape::Transition %s --> %s", escState.Name(), s.Name())
|
||||
escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
return escState.parser.escDispatch()
|
||||
case escState.parser.escapeIntermediate:
|
||||
return escState.parser.collectInter()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (escState escapeState) Enter() error {
|
||||
escState.parser.clear()
|
||||
return nil
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package ansiterm
|
||||
|
||||
type AnsiEventHandler interface {
|
||||
// Print
|
||||
Print(b byte) error
|
||||
|
||||
// Execute C0 commands
|
||||
Execute(b byte) error
|
||||
|
||||
// CUrsor Up
|
||||
CUU(int) error
|
||||
|
||||
// CUrsor Down
|
||||
CUD(int) error
|
||||
|
||||
// CUrsor Forward
|
||||
CUF(int) error
|
||||
|
||||
// CUrsor Backward
|
||||
CUB(int) error
|
||||
|
||||
// Cursor to Next Line
|
||||
CNL(int) error
|
||||
|
||||
// Cursor to Previous Line
|
||||
CPL(int) error
|
||||
|
||||
// Cursor Horizontal position Absolute
|
||||
CHA(int) error
|
||||
|
||||
// Vertical line Position Absolute
|
||||
VPA(int) error
|
||||
|
||||
// CUrsor Position
|
||||
CUP(int, int) error
|
||||
|
||||
// Horizontal and Vertical Position (depends on PUM)
|
||||
HVP(int, int) error
|
||||
|
||||
// Text Cursor Enable Mode
|
||||
DECTCEM(bool) error
|
||||
|
||||
// Origin Mode
|
||||
DECOM(bool) error
|
||||
|
||||
// 132 Column Mode
|
||||
DECCOLM(bool) error
|
||||
|
||||
// Erase in Display
|
||||
ED(int) error
|
||||
|
||||
// Erase in Line
|
||||
EL(int) error
|
||||
|
||||
// Insert Line
|
||||
IL(int) error
|
||||
|
||||
// Delete Line
|
||||
DL(int) error
|
||||
|
||||
// Insert Character
|
||||
ICH(int) error
|
||||
|
||||
// Delete Character
|
||||
DCH(int) error
|
||||
|
||||
// Set Graphics Rendition
|
||||
SGR([]int) error
|
||||
|
||||
// Pan Down
|
||||
SU(int) error
|
||||
|
||||
// Pan Up
|
||||
SD(int) error
|
||||
|
||||
// Device Attributes
|
||||
DA([]string) error
|
||||
|
||||
// Set Top and Bottom Margins
|
||||
DECSTBM(int, int) error
|
||||
|
||||
// Index
|
||||
IND() error
|
||||
|
||||
// Reverse Index
|
||||
RI() error
|
||||
|
||||
// Flush updates from previous commands
|
||||
Flush() error
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package ansiterm
|
||||
|
||||
type groundState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (gs groundState) Handle(b byte) (s state, e error) {
|
||||
gs.parser.context.currentChar = b
|
||||
|
||||
nextState, err := gs.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(printables, b):
|
||||
return gs, gs.parser.print()
|
||||
|
||||
case sliceContains(executors, b):
|
||||
return gs, gs.parser.execute()
|
||||
}
|
||||
|
||||
return gs, nil
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package ansiterm
|
||||
|
||||
type oscStringState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (oscState oscStringState) Handle(b byte) (s state, e error) {
|
||||
logger.Infof("OscString::Handle %#x", b)
|
||||
nextState, err := oscState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isOscStringTerminator(b):
|
||||
return oscState.parser.ground, nil
|
||||
}
|
||||
|
||||
return oscState, nil
|
||||
}
|
||||
|
||||
// See below for OSC string terminators for linux
|
||||
// http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||
func isOscStringTerminator(b byte) bool {
|
||||
|
||||
if b == ANSI_BEL || b == 0x5C {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
var logger *logrus.Logger
|
||||
|
||||
type AnsiParser struct {
|
||||
currState state
|
||||
eventHandler AnsiEventHandler
|
||||
context *ansiContext
|
||||
csiEntry state
|
||||
csiParam state
|
||||
dcsEntry state
|
||||
escape state
|
||||
escapeIntermediate state
|
||||
error state
|
||||
ground state
|
||||
oscString state
|
||||
stateMap []state
|
||||
}
|
||||
|
||||
func CreateParser(initialState string, evtHandler AnsiEventHandler) *AnsiParser {
|
||||
logFile := ioutil.Discard
|
||||
|
||||
if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" {
|
||||
logFile, _ = os.Create("ansiParser.log")
|
||||
}
|
||||
|
||||
logger = &logrus.Logger{
|
||||
Out: logFile,
|
||||
Formatter: new(logrus.TextFormatter),
|
||||
Level: logrus.InfoLevel,
|
||||
}
|
||||
|
||||
parser := &AnsiParser{
|
||||
eventHandler: evtHandler,
|
||||
context: &ansiContext{},
|
||||
}
|
||||
|
||||
parser.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: parser}}
|
||||
parser.csiParam = csiParamState{baseState{name: "CsiParam", parser: parser}}
|
||||
parser.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: parser}}
|
||||
parser.escape = escapeState{baseState{name: "Escape", parser: parser}}
|
||||
parser.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: parser}}
|
||||
parser.error = errorState{baseState{name: "Error", parser: parser}}
|
||||
parser.ground = groundState{baseState{name: "Ground", parser: parser}}
|
||||
parser.oscString = oscStringState{baseState{name: "OscString", parser: parser}}
|
||||
|
||||
parser.stateMap = []state{
|
||||
parser.csiEntry,
|
||||
parser.csiParam,
|
||||
parser.dcsEntry,
|
||||
parser.escape,
|
||||
parser.escapeIntermediate,
|
||||
parser.error,
|
||||
parser.ground,
|
||||
parser.oscString,
|
||||
}
|
||||
|
||||
parser.currState = getState(initialState, parser.stateMap)
|
||||
|
||||
logger.Infof("CreateParser: parser %p", parser)
|
||||
return parser
|
||||
}
|
||||
|
||||
func getState(name string, states []state) state {
|
||||
for _, el := range states {
|
||||
if el.Name() == name {
|
||||
return el
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) Parse(bytes []byte) (int, error) {
|
||||
for i, b := range bytes {
|
||||
if err := ap.handle(b); err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
return len(bytes), ap.eventHandler.Flush()
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) handle(b byte) error {
|
||||
ap.context.currentChar = b
|
||||
newState, err := ap.currState.Handle(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newState == nil {
|
||||
logger.Warning("newState is nil")
|
||||
return errors.New("New state of 'nil' is invalid.")
|
||||
}
|
||||
|
||||
if newState != ap.currState {
|
||||
if err := ap.changeState(newState); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) changeState(newState state) error {
|
||||
logger.Infof("ChangeState %s --> %s", ap.currState.Name(), newState.Name())
|
||||
|
||||
// Exit old state
|
||||
if err := ap.currState.Exit(); err != nil {
|
||||
logger.Infof("Exit state '%s' failed with : '%v'", ap.currState.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform transition action
|
||||
if err := ap.currState.Transition(newState); err != nil {
|
||||
logger.Infof("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Enter new state
|
||||
if err := newState.Enter(); err != nil {
|
||||
logger.Infof("Enter state '%s' failed with: '%v'", newState.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
ap.currState = newState
|
||||
return nil
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func parseParams(bytes []byte) ([]string, error) {
|
||||
paramBuff := make([]byte, 0, 0)
|
||||
params := []string{}
|
||||
|
||||
for _, v := range bytes {
|
||||
if v == ';' {
|
||||
if len(paramBuff) > 0 {
|
||||
// Completed parameter, append it to the list
|
||||
s := string(paramBuff)
|
||||
params = append(params, s)
|
||||
paramBuff = make([]byte, 0, 0)
|
||||
}
|
||||
} else {
|
||||
paramBuff = append(paramBuff, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Last parameter may not be terminated with ';'
|
||||
if len(paramBuff) > 0 {
|
||||
s := string(paramBuff)
|
||||
params = append(params, s)
|
||||
}
|
||||
|
||||
logger.Infof("Parsed params: %v with length: %d", params, len(params))
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func parseCmd(context ansiContext) (string, error) {
|
||||
return string(context.currentChar), nil
|
||||
}
|
||||
|
||||
func getInt(params []string, dflt int) int {
|
||||
i := getInts(params, 1, dflt)[0]
|
||||
logger.Infof("getInt: %v", i)
|
||||
return i
|
||||
}
|
||||
|
||||
func getInts(params []string, minCount int, dflt int) []int {
|
||||
ints := []int{}
|
||||
|
||||
for _, v := range params {
|
||||
i, _ := strconv.Atoi(v)
|
||||
// Zero is mapped to the default value in VT100.
|
||||
if i == 0 {
|
||||
i = dflt
|
||||
}
|
||||
ints = append(ints, i)
|
||||
}
|
||||
|
||||
if len(ints) < minCount {
|
||||
remaining := minCount - len(ints)
|
||||
for i := 0; i < remaining; i++ {
|
||||
ints = append(ints, dflt)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("getInts: %v", ints)
|
||||
|
||||
return ints
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) modeDispatch(param string, set bool) error {
|
||||
switch param {
|
||||
case "?3":
|
||||
return ap.eventHandler.DECCOLM(set)
|
||||
case "?6":
|
||||
return ap.eventHandler.DECOM(set)
|
||||
case "?25":
|
||||
return ap.eventHandler.DECTCEM(set)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) hDispatch(params []string) error {
|
||||
if len(params) == 1 {
|
||||
return ap.modeDispatch(params[0], true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) lDispatch(params []string) error {
|
||||
if len(params) == 1 {
|
||||
return ap.modeDispatch(params[0], false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEraseParam(params []string) int {
|
||||
param := getInt(params, 0)
|
||||
if param < 0 || 3 < param {
|
||||
param = 0
|
||||
}
|
||||
|
||||
return param
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (ap *AnsiParser) collectParam() error {
|
||||
currChar := ap.context.currentChar
|
||||
logger.Infof("collectParam %#x", currChar)
|
||||
ap.context.paramBuffer = append(ap.context.paramBuffer, currChar)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) collectInter() error {
|
||||
currChar := ap.context.currentChar
|
||||
logger.Infof("collectInter %#x", currChar)
|
||||
ap.context.paramBuffer = append(ap.context.interBuffer, currChar)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) escDispatch() error {
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
intermeds := ap.context.interBuffer
|
||||
logger.Infof("escDispatch currentChar: %#x", ap.context.currentChar)
|
||||
logger.Infof("escDispatch: %v(%v)", cmd, intermeds)
|
||||
|
||||
switch cmd {
|
||||
case "D": // IND
|
||||
return ap.eventHandler.IND()
|
||||
case "E": // NEL, equivalent to CRLF
|
||||
err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN)
|
||||
if err == nil {
|
||||
err = ap.eventHandler.Execute(ANSI_LINE_FEED)
|
||||
}
|
||||
return err
|
||||
case "M": // RI
|
||||
return ap.eventHandler.RI()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) csiDispatch() error {
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
params, _ := parseParams(ap.context.paramBuffer)
|
||||
|
||||
logger.Infof("csiDispatch: %v(%v)", cmd, params)
|
||||
|
||||
switch cmd {
|
||||
case "@":
|
||||
return ap.eventHandler.ICH(getInt(params, 1))
|
||||
case "A":
|
||||
return ap.eventHandler.CUU(getInt(params, 1))
|
||||
case "B":
|
||||
return ap.eventHandler.CUD(getInt(params, 1))
|
||||
case "C":
|
||||
return ap.eventHandler.CUF(getInt(params, 1))
|
||||
case "D":
|
||||
return ap.eventHandler.CUB(getInt(params, 1))
|
||||
case "E":
|
||||
return ap.eventHandler.CNL(getInt(params, 1))
|
||||
case "F":
|
||||
return ap.eventHandler.CPL(getInt(params, 1))
|
||||
case "G":
|
||||
return ap.eventHandler.CHA(getInt(params, 1))
|
||||
case "H":
|
||||
ints := getInts(params, 2, 1)
|
||||
x, y := ints[0], ints[1]
|
||||
return ap.eventHandler.CUP(x, y)
|
||||
case "J":
|
||||
param := getEraseParam(params)
|
||||
return ap.eventHandler.ED(param)
|
||||
case "K":
|
||||
param := getEraseParam(params)
|
||||
return ap.eventHandler.EL(param)
|
||||
case "L":
|
||||
return ap.eventHandler.IL(getInt(params, 1))
|
||||
case "M":
|
||||
return ap.eventHandler.DL(getInt(params, 1))
|
||||
case "P":
|
||||
return ap.eventHandler.DCH(getInt(params, 1))
|
||||
case "S":
|
||||
return ap.eventHandler.SU(getInt(params, 1))
|
||||
case "T":
|
||||
return ap.eventHandler.SD(getInt(params, 1))
|
||||
case "c":
|
||||
return ap.eventHandler.DA(params)
|
||||
case "d":
|
||||
return ap.eventHandler.VPA(getInt(params, 1))
|
||||
case "f":
|
||||
ints := getInts(params, 2, 1)
|
||||
x, y := ints[0], ints[1]
|
||||
return ap.eventHandler.HVP(x, y)
|
||||
case "h":
|
||||
return ap.hDispatch(params)
|
||||
case "l":
|
||||
return ap.lDispatch(params)
|
||||
case "m":
|
||||
return ap.eventHandler.SGR(getInts(params, 1, 0))
|
||||
case "r":
|
||||
ints := getInts(params, 2, 1)
|
||||
top, bottom := ints[0], ints[1]
|
||||
return ap.eventHandler.DECSTBM(top, bottom)
|
||||
default:
|
||||
logger.Errorf(fmt.Sprintf("Unsupported CSI command: '%s', with full context: %v", cmd, ap.context))
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) print() error {
|
||||
return ap.eventHandler.Print(ap.context.currentChar)
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) clear() error {
|
||||
ap.context = &ansiContext{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) execute() error {
|
||||
return ap.eventHandler.Execute(ap.context.currentChar)
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStateTransitions(t *testing.T) {
|
||||
stateTransitionHelper(t, "CsiEntry", "Ground", alphabetics)
|
||||
stateTransitionHelper(t, "CsiEntry", "CsiParam", csiCollectables)
|
||||
stateTransitionHelper(t, "Escape", "CsiEntry", []byte{ANSI_ESCAPE_SECONDARY})
|
||||
stateTransitionHelper(t, "Escape", "OscString", []byte{0x5D})
|
||||
stateTransitionHelper(t, "Escape", "Ground", escapeToGroundBytes)
|
||||
stateTransitionHelper(t, "Escape", "EscapeIntermediate", intermeds)
|
||||
stateTransitionHelper(t, "EscapeIntermediate", "EscapeIntermediate", intermeds)
|
||||
stateTransitionHelper(t, "EscapeIntermediate", "EscapeIntermediate", executors)
|
||||
stateTransitionHelper(t, "EscapeIntermediate", "Ground", escapeIntermediateToGroundBytes)
|
||||
stateTransitionHelper(t, "OscString", "Ground", []byte{ANSI_BEL})
|
||||
stateTransitionHelper(t, "OscString", "Ground", []byte{0x5C})
|
||||
stateTransitionHelper(t, "Ground", "Ground", executors)
|
||||
}
|
||||
|
||||
func TestAnyToX(t *testing.T) {
|
||||
anyToXHelper(t, []byte{ANSI_ESCAPE_PRIMARY}, "Escape")
|
||||
anyToXHelper(t, []byte{DCS_ENTRY}, "DcsEntry")
|
||||
anyToXHelper(t, []byte{OSC_STRING}, "OscString")
|
||||
anyToXHelper(t, []byte{CSI_ENTRY}, "CsiEntry")
|
||||
anyToXHelper(t, toGroundBytes, "Ground")
|
||||
}
|
||||
|
||||
func TestCollectCsiParams(t *testing.T) {
|
||||
parser, _ := createTestParser("CsiEntry")
|
||||
parser.Parse(csiCollectables)
|
||||
|
||||
buffer := parser.context.paramBuffer
|
||||
bufferCount := len(buffer)
|
||||
|
||||
if bufferCount != len(csiCollectables) {
|
||||
t.Errorf("Buffer: %v", buffer)
|
||||
t.Errorf("CsiParams: %v", csiCollectables)
|
||||
t.Errorf("Buffer count failure: %d != %d", bufferCount, len(csiParams))
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range csiCollectables {
|
||||
if v != buffer[i] {
|
||||
t.Errorf("Buffer: %v", buffer)
|
||||
t.Errorf("CsiParams: %v", csiParams)
|
||||
t.Errorf("Mismatch at buffer[%d] = %d", i, buffer[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParams(t *testing.T) {
|
||||
parseParamsHelper(t, []byte{}, []string{})
|
||||
parseParamsHelper(t, []byte{';'}, []string{})
|
||||
parseParamsHelper(t, []byte{';', ';'}, []string{})
|
||||
parseParamsHelper(t, []byte{'7'}, []string{"7"})
|
||||
parseParamsHelper(t, []byte{'7', ';'}, []string{"7"})
|
||||
parseParamsHelper(t, []byte{'7', ';', ';'}, []string{"7"})
|
||||
parseParamsHelper(t, []byte{'7', ';', ';', '8'}, []string{"7", "8"})
|
||||
parseParamsHelper(t, []byte{'7', ';', '8', ';'}, []string{"7", "8"})
|
||||
parseParamsHelper(t, []byte{'7', ';', ';', '8', ';', ';'}, []string{"7", "8"})
|
||||
parseParamsHelper(t, []byte{'7', '8'}, []string{"78"})
|
||||
parseParamsHelper(t, []byte{'7', '8', ';'}, []string{"78"})
|
||||
parseParamsHelper(t, []byte{'7', '8', ';', '9', '0'}, []string{"78", "90"})
|
||||
parseParamsHelper(t, []byte{'7', '8', ';', ';', '9', '0'}, []string{"78", "90"})
|
||||
parseParamsHelper(t, []byte{'7', '8', ';', '9', '0', ';'}, []string{"78", "90"})
|
||||
parseParamsHelper(t, []byte{'7', '8', ';', '9', '0', ';', ';'}, []string{"78", "90"})
|
||||
}
|
||||
|
||||
func TestCursor(t *testing.T) {
|
||||
cursorSingleParamHelper(t, 'A', "CUU")
|
||||
cursorSingleParamHelper(t, 'B', "CUD")
|
||||
cursorSingleParamHelper(t, 'C', "CUF")
|
||||
cursorSingleParamHelper(t, 'D', "CUB")
|
||||
cursorSingleParamHelper(t, 'E', "CNL")
|
||||
cursorSingleParamHelper(t, 'F', "CPL")
|
||||
cursorSingleParamHelper(t, 'G', "CHA")
|
||||
cursorTwoParamHelper(t, 'H', "CUP")
|
||||
cursorTwoParamHelper(t, 'f', "HVP")
|
||||
funcCallParamHelper(t, []byte{'?', '2', '5', 'h'}, "CsiEntry", "Ground", []string{"DECTCEM([true])"})
|
||||
funcCallParamHelper(t, []byte{'?', '2', '5', 'l'}, "CsiEntry", "Ground", []string{"DECTCEM([false])"})
|
||||
}
|
||||
|
||||
func TestErase(t *testing.T) {
|
||||
// Erase in Display
|
||||
eraseHelper(t, 'J', "ED")
|
||||
|
||||
// Erase in Line
|
||||
eraseHelper(t, 'K', "EL")
|
||||
}
|
||||
|
||||
func TestSelectGraphicRendition(t *testing.T) {
|
||||
funcCallParamHelper(t, []byte{'m'}, "CsiEntry", "Ground", []string{"SGR([0])"})
|
||||
funcCallParamHelper(t, []byte{'0', 'm'}, "CsiEntry", "Ground", []string{"SGR([0])"})
|
||||
funcCallParamHelper(t, []byte{'0', ';', '1', 'm'}, "CsiEntry", "Ground", []string{"SGR([0 1])"})
|
||||
funcCallParamHelper(t, []byte{'0', ';', '1', ';', '2', 'm'}, "CsiEntry", "Ground", []string{"SGR([0 1 2])"})
|
||||
}
|
||||
|
||||
func TestScroll(t *testing.T) {
|
||||
scrollHelper(t, 'S', "SU")
|
||||
scrollHelper(t, 'T', "SD")
|
||||
}
|
||||
|
||||
func TestPrint(t *testing.T) {
|
||||
parser, evtHandler := createTestParser("Ground")
|
||||
parser.Parse(printables)
|
||||
validateState(t, parser.currState, "Ground")
|
||||
|
||||
for i, v := range printables {
|
||||
expectedCall := fmt.Sprintf("Print([%s])", string(v))
|
||||
actualCall := evtHandler.FunctionCalls[i]
|
||||
if actualCall != expectedCall {
|
||||
t.Errorf("Actual != Expected: %v != %v at %d", actualCall, expectedCall, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
p, _ := createTestParser("Ground")
|
||||
fillContext(p.context)
|
||||
p.clear()
|
||||
validateEmptyContext(t, p.context)
|
||||
}
|
||||
|
||||
func TestClearOnStateChange(t *testing.T) {
|
||||
clearOnStateChangeHelper(t, "Ground", "Escape", []byte{ANSI_ESCAPE_PRIMARY})
|
||||
clearOnStateChangeHelper(t, "Ground", "CsiEntry", []byte{CSI_ENTRY})
|
||||
}
|
||||
|
||||
func TestC0(t *testing.T) {
|
||||
expectedCall := "Execute([" + string(ANSI_LINE_FEED) + "])"
|
||||
c0Helper(t, []byte{ANSI_LINE_FEED}, "Ground", []string{expectedCall})
|
||||
expectedCall = "Execute([" + string(ANSI_CARRIAGE_RETURN) + "])"
|
||||
c0Helper(t, []byte{ANSI_CARRIAGE_RETURN}, "Ground", []string{expectedCall})
|
||||
}
|
||||
|
||||
func TestEscDispatch(t *testing.T) {
|
||||
funcCallParamHelper(t, []byte{'M'}, "Escape", "Ground", []string{"RI([])"})
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getStateNames() []string {
|
||||
parser, _ := createTestParser("Ground")
|
||||
|
||||
stateNames := []string{}
|
||||
for _, state := range parser.stateMap {
|
||||
stateNames = append(stateNames, state.Name())
|
||||
}
|
||||
|
||||
return stateNames
|
||||
}
|
||||
|
||||
func stateTransitionHelper(t *testing.T, start string, end string, bytes []byte) {
|
||||
for _, b := range bytes {
|
||||
bytes := []byte{byte(b)}
|
||||
parser, _ := createTestParser(start)
|
||||
parser.Parse(bytes)
|
||||
validateState(t, parser.currState, end)
|
||||
}
|
||||
}
|
||||
|
||||
func anyToXHelper(t *testing.T, bytes []byte, expectedState string) {
|
||||
for _, s := range getStateNames() {
|
||||
stateTransitionHelper(t, s, expectedState, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func funcCallParamHelper(t *testing.T, bytes []byte, start string, expected string, expectedCalls []string) {
|
||||
parser, evtHandler := createTestParser(start)
|
||||
parser.Parse(bytes)
|
||||
validateState(t, parser.currState, expected)
|
||||
validateFuncCalls(t, evtHandler.FunctionCalls, expectedCalls)
|
||||
}
|
||||
|
||||
func parseParamsHelper(t *testing.T, bytes []byte, expectedParams []string) {
|
||||
params, err := parseParams(bytes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Parameter parse error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(params) != len(expectedParams) {
|
||||
t.Errorf("Parsed parameters: %v", params)
|
||||
t.Errorf("Expected parameters: %v", expectedParams)
|
||||
t.Errorf("Parameter length failure: %d != %d", len(params), len(expectedParams))
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range expectedParams {
|
||||
if v != params[i] {
|
||||
t.Errorf("Parsed parameters: %v", params)
|
||||
t.Errorf("Expected parameters: %v", expectedParams)
|
||||
t.Errorf("Parameter parse failure: %s != %s at position %d", v, params[i], i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cursorSingleParamHelper(t *testing.T, command byte, funcName string) {
|
||||
funcCallParamHelper(t, []byte{command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'0', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([2])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', '3', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([23])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', ';', '3', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([2])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', ';', '3', ';', '4', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([2])", funcName)})
|
||||
}
|
||||
|
||||
func cursorTwoParamHelper(t *testing.T, command byte, funcName string) {
|
||||
funcCallParamHelper(t, []byte{command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1 1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'0', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1 1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([2 1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', '3', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([23 1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', ';', '3', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([2 3])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', ';', '3', ';', '4', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([2 3])", funcName)})
|
||||
}
|
||||
|
||||
func eraseHelper(t *testing.T, command byte, funcName string) {
|
||||
funcCallParamHelper(t, []byte{command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([0])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'0', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([0])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'1', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'2', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([2])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'3', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([3])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'4', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([0])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'1', ';', '2', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1])", funcName)})
|
||||
}
|
||||
|
||||
func scrollHelper(t *testing.T, command byte, funcName string) {
|
||||
funcCallParamHelper(t, []byte{command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'0', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'1', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([1])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'5', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([5])", funcName)})
|
||||
funcCallParamHelper(t, []byte{'4', ';', '6', command}, "CsiEntry", "Ground", []string{fmt.Sprintf("%s([4])", funcName)})
|
||||
}
|
||||
|
||||
func clearOnStateChangeHelper(t *testing.T, start string, end string, bytes []byte) {
|
||||
p, _ := createTestParser(start)
|
||||
fillContext(p.context)
|
||||
p.Parse(bytes)
|
||||
validateState(t, p.currState, end)
|
||||
validateEmptyContext(t, p.context)
|
||||
}
|
||||
|
||||
func c0Helper(t *testing.T, bytes []byte, expectedState string, expectedCalls []string) {
|
||||
parser, evtHandler := createTestParser("Ground")
|
||||
parser.Parse(bytes)
|
||||
validateState(t, parser.currState, expectedState)
|
||||
validateFuncCalls(t, evtHandler.FunctionCalls, expectedCalls)
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func createTestParser(s string) (*AnsiParser, *TestAnsiEventHandler) {
|
||||
evtHandler := CreateTestAnsiEventHandler()
|
||||
parser := CreateParser(s, evtHandler)
|
||||
|
||||
return parser, evtHandler
|
||||
}
|
||||
|
||||
func validateState(t *testing.T, actualState state, expectedStateName string) {
|
||||
actualName := "Nil"
|
||||
|
||||
if actualState != nil {
|
||||
actualName = actualState.Name()
|
||||
}
|
||||
|
||||
if actualName != expectedStateName {
|
||||
t.Errorf("Invalid state: '%s' != '%s'", actualName, expectedStateName)
|
||||
}
|
||||
}
|
||||
|
||||
func validateFuncCalls(t *testing.T, actualCalls []string, expectedCalls []string) {
|
||||
actualCount := len(actualCalls)
|
||||
expectedCount := len(expectedCalls)
|
||||
|
||||
if actualCount != expectedCount {
|
||||
t.Errorf("Actual calls: %v", actualCalls)
|
||||
t.Errorf("Expected calls: %v", expectedCalls)
|
||||
t.Errorf("Call count error: %d != %d", actualCount, expectedCount)
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range actualCalls {
|
||||
if v != expectedCalls[i] {
|
||||
t.Errorf("Actual calls: %v", actualCalls)
|
||||
t.Errorf("Expected calls: %v", expectedCalls)
|
||||
t.Errorf("Mismatched calls: %s != %s with lengths %d and %d", v, expectedCalls[i], len(v), len(expectedCalls[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fillContext(context *ansiContext) {
|
||||
context.currentChar = 'A'
|
||||
context.paramBuffer = []byte{'C', 'D', 'E'}
|
||||
context.interBuffer = []byte{'F', 'G', 'H'}
|
||||
}
|
||||
|
||||
func validateEmptyContext(t *testing.T, context *ansiContext) {
|
||||
var expectedCurrChar byte = 0x0
|
||||
if context.currentChar != expectedCurrChar {
|
||||
t.Errorf("Currentchar mismatch '%#x' != '%#x'", context.currentChar, expectedCurrChar)
|
||||
}
|
||||
|
||||
if len(context.paramBuffer) != 0 {
|
||||
t.Errorf("Non-empty parameter buffer: %v", context.paramBuffer)
|
||||
}
|
||||
|
||||
if len(context.paramBuffer) != 0 {
|
||||
t.Errorf("Non-empty intermediate buffer: %v", context.interBuffer)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package ansiterm
|
||||
|
||||
type stateID int
|
||||
|
||||
type state interface {
|
||||
Enter() error
|
||||
Exit() error
|
||||
Handle(byte) (state, error)
|
||||
Name() string
|
||||
Transition(state) error
|
||||
}
|
||||
|
||||
type baseState struct {
|
||||
name string
|
||||
parser *AnsiParser
|
||||
}
|
||||
|
||||
func (base baseState) Enter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (base baseState) Exit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (base baseState) Handle(b byte) (s state, e error) {
|
||||
|
||||
switch {
|
||||
case b == CSI_ENTRY:
|
||||
return base.parser.csiEntry, nil
|
||||
case b == DCS_ENTRY:
|
||||
return base.parser.dcsEntry, nil
|
||||
case b == ANSI_ESCAPE_PRIMARY:
|
||||
return base.parser.escape, nil
|
||||
case b == OSC_STRING:
|
||||
return base.parser.oscString, nil
|
||||
case sliceContains(toGroundBytes, b):
|
||||
return base.parser.ground, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (base baseState) Name() string {
|
||||
return base.name
|
||||
}
|
||||
|
||||
func (base baseState) Transition(s state) error {
|
||||
if s == base.parser.ground {
|
||||
execBytes := []byte{0x18}
|
||||
execBytes = append(execBytes, 0x1A)
|
||||
execBytes = append(execBytes, getByteRange(0x80, 0x8F)...)
|
||||
execBytes = append(execBytes, getByteRange(0x91, 0x97)...)
|
||||
execBytes = append(execBytes, 0x99)
|
||||
execBytes = append(execBytes, 0x9A)
|
||||
|
||||
if sliceContains(execBytes, base.parser.context.currentChar) {
|
||||
return base.parser.execute()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dcsEntryState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
type errorState struct {
|
||||
baseState
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TestAnsiEventHandler struct {
|
||||
FunctionCalls []string
|
||||
}
|
||||
|
||||
func CreateTestAnsiEventHandler() *TestAnsiEventHandler {
|
||||
evtHandler := TestAnsiEventHandler{}
|
||||
evtHandler.FunctionCalls = make([]string, 0)
|
||||
return &evtHandler
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) recordCall(call string, params []string) {
|
||||
s := fmt.Sprintf("%s(%v)", call, params)
|
||||
h.FunctionCalls = append(h.FunctionCalls, s)
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) Print(b byte) error {
|
||||
h.recordCall("Print", []string{string(b)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) Execute(b byte) error {
|
||||
h.recordCall("Execute", []string{string(b)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CUU(param int) error {
|
||||
h.recordCall("CUU", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CUD(param int) error {
|
||||
h.recordCall("CUD", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CUF(param int) error {
|
||||
h.recordCall("CUF", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CUB(param int) error {
|
||||
h.recordCall("CUB", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CNL(param int) error {
|
||||
h.recordCall("CNL", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CPL(param int) error {
|
||||
h.recordCall("CPL", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CHA(param int) error {
|
||||
h.recordCall("CHA", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) VPA(param int) error {
|
||||
h.recordCall("VPA", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) CUP(x int, y int) error {
|
||||
xS, yS := strconv.Itoa(x), strconv.Itoa(y)
|
||||
h.recordCall("CUP", []string{xS, yS})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) HVP(x int, y int) error {
|
||||
xS, yS := strconv.Itoa(x), strconv.Itoa(y)
|
||||
h.recordCall("HVP", []string{xS, yS})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) DECTCEM(visible bool) error {
|
||||
h.recordCall("DECTCEM", []string{strconv.FormatBool(visible)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) DECOM(visible bool) error {
|
||||
h.recordCall("DECOM", []string{strconv.FormatBool(visible)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) DECCOLM(use132 bool) error {
|
||||
h.recordCall("DECOLM", []string{strconv.FormatBool(use132)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) ED(param int) error {
|
||||
h.recordCall("ED", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) EL(param int) error {
|
||||
h.recordCall("EL", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) IL(param int) error {
|
||||
h.recordCall("IL", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) DL(param int) error {
|
||||
h.recordCall("DL", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) ICH(param int) error {
|
||||
h.recordCall("ICH", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) DCH(param int) error {
|
||||
h.recordCall("DCH", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) SGR(params []int) error {
|
||||
strings := []string{}
|
||||
for _, v := range params {
|
||||
strings = append(strings, strconv.Itoa(v))
|
||||
}
|
||||
|
||||
h.recordCall("SGR", strings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) SU(param int) error {
|
||||
h.recordCall("SU", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) SD(param int) error {
|
||||
h.recordCall("SD", []string{strconv.Itoa(param)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) DA(params []string) error {
|
||||
h.recordCall("DA", params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) DECSTBM(top int, bottom int) error {
|
||||
topS, bottomS := strconv.Itoa(top), strconv.Itoa(bottom)
|
||||
h.recordCall("DECSTBM", []string{topS, bottomS})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) RI() error {
|
||||
h.recordCall("RI", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) IND() error {
|
||||
h.recordCall("IND", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TestAnsiEventHandler) Flush() error {
|
||||
return nil
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func sliceContains(bytes []byte, b byte) bool {
|
||||
for _, v := range bytes {
|
||||
if v == b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertBytesToInteger(bytes []byte) int {
|
||||
s := string(bytes)
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/Azure/go-ansiterm"
|
||||
)
|
||||
|
||||
// Windows keyboard constants
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx.
|
||||
const (
|
||||
VK_PRIOR = 0x21 // PAGE UP key
|
||||
VK_NEXT = 0x22 // PAGE DOWN key
|
||||
VK_END = 0x23 // END key
|
||||
VK_HOME = 0x24 // HOME key
|
||||
VK_LEFT = 0x25 // LEFT ARROW key
|
||||
VK_UP = 0x26 // UP ARROW key
|
||||
VK_RIGHT = 0x27 // RIGHT ARROW key
|
||||
VK_DOWN = 0x28 // DOWN ARROW key
|
||||
VK_SELECT = 0x29 // SELECT key
|
||||
VK_PRINT = 0x2A // PRINT key
|
||||
VK_EXECUTE = 0x2B // EXECUTE key
|
||||
VK_SNAPSHOT = 0x2C // PRINT SCREEN key
|
||||
VK_INSERT = 0x2D // INS key
|
||||
VK_DELETE = 0x2E // DEL key
|
||||
VK_HELP = 0x2F // HELP key
|
||||
VK_F1 = 0x70 // F1 key
|
||||
VK_F2 = 0x71 // F2 key
|
||||
VK_F3 = 0x72 // F3 key
|
||||
VK_F4 = 0x73 // F4 key
|
||||
VK_F5 = 0x74 // F5 key
|
||||
VK_F6 = 0x75 // F6 key
|
||||
VK_F7 = 0x76 // F7 key
|
||||
VK_F8 = 0x77 // F8 key
|
||||
VK_F9 = 0x78 // F9 key
|
||||
VK_F10 = 0x79 // F10 key
|
||||
VK_F11 = 0x7A // F11 key
|
||||
VK_F12 = 0x7B // F12 key
|
||||
|
||||
RIGHT_ALT_PRESSED = 0x0001
|
||||
LEFT_ALT_PRESSED = 0x0002
|
||||
RIGHT_CTRL_PRESSED = 0x0004
|
||||
LEFT_CTRL_PRESSED = 0x0008
|
||||
SHIFT_PRESSED = 0x0010
|
||||
NUMLOCK_ON = 0x0020
|
||||
SCROLLLOCK_ON = 0x0040
|
||||
CAPSLOCK_ON = 0x0080
|
||||
ENHANCED_KEY = 0x0100
|
||||
)
|
||||
|
||||
type ansiCommand struct {
|
||||
CommandBytes []byte
|
||||
Command string
|
||||
Parameters []string
|
||||
IsSpecial bool
|
||||
}
|
||||
|
||||
func newAnsiCommand(command []byte) *ansiCommand {
|
||||
|
||||
if isCharacterSelectionCmdChar(command[1]) {
|
||||
// Is Character Set Selection commands
|
||||
return &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command),
|
||||
IsSpecial: true,
|
||||
}
|
||||
}
|
||||
|
||||
// last char is command character
|
||||
lastCharIndex := len(command) - 1
|
||||
|
||||
ac := &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command[lastCharIndex]),
|
||||
IsSpecial: false,
|
||||
}
|
||||
|
||||
// more than a single escape
|
||||
if lastCharIndex != 0 {
|
||||
start := 1
|
||||
// skip if double char escape sequence
|
||||
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
|
||||
start++
|
||||
}
|
||||
// convert this to GetNextParam method
|
||||
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
|
||||
if index < 0 || index >= len(ac.Parameters) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return int16(param)
|
||||
}
|
||||
|
||||
func (ac *ansiCommand) String() string {
|
||||
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
|
||||
bytesToHex(ac.CommandBytes),
|
||||
ac.Command,
|
||||
strings.Join(ac.Parameters, "\",\""))
|
||||
}
|
||||
|
||||
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
|
||||
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
|
||||
func isAnsiCommandChar(b byte) bool {
|
||||
switch {
|
||||
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
|
||||
return true
|
||||
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
|
||||
// non-CSI escape sequence terminator
|
||||
return true
|
||||
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
|
||||
// String escape sequence terminator
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isXtermOscSequence(command []byte, current byte) bool {
|
||||
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
|
||||
}
|
||||
|
||||
func isCharacterSelectionCmdChar(b byte) bool {
|
||||
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
|
||||
}
|
||||
|
||||
// bytesToHex converts a slice of bytes to a human-readable string.
|
||||
func bytesToHex(b []byte) string {
|
||||
hex := make([]string, len(b))
|
||||
for i, ch := range b {
|
||||
hex[i] = fmt.Sprintf("%X", ch)
|
||||
}
|
||||
return strings.Join(hex, "")
|
||||
}
|
||||
|
||||
// ensureInRange adjusts the passed value, if necessary, to ensure it is within
|
||||
// the passed min / max range.
|
||||
func ensureInRange(n int16, min int16, max int16) int16 {
|
||||
if n < min {
|
||||
return min
|
||||
} else if n > max {
|
||||
return max
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
func GetStdFile(nFile int) (*os.File, uintptr) {
|
||||
var file *os.File
|
||||
switch nFile {
|
||||
case syscall.STD_INPUT_HANDLE:
|
||||
file = os.Stdin
|
||||
case syscall.STD_OUTPUT_HANDLE:
|
||||
file = os.Stdout
|
||||
case syscall.STD_ERROR_HANDLE:
|
||||
file = os.Stderr
|
||||
default:
|
||||
panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile))
|
||||
}
|
||||
|
||||
fd, err := syscall.GetStdHandle(nFile)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Invalid standard handle indentifier: %v -- %v", nFile, err))
|
||||
}
|
||||
|
||||
return file, uintptr(fd)
|
||||
}
|
@ -0,0 +1,322 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//===========================================================================================================
|
||||
// IMPORTANT NOTE:
|
||||
//
|
||||
// The methods below make extensive use of the "unsafe" package to obtain the required pointers.
|
||||
// Beginning in Go 1.3, the garbage collector may release local variables (e.g., incoming arguments, stack
|
||||
// variables) the pointers reference *before* the API completes.
|
||||
//
|
||||
// As a result, in those cases, the code must hint that the variables remain in active by invoking the
|
||||
// dummy method "use" (see below). Newer versions of Go are planned to change the mechanism to no longer
|
||||
// require unsafe pointers.
|
||||
//
|
||||
// If you add or modify methods, ENSURE protection of local variables through the "use" builtin to inform
|
||||
// the garbage collector the variables remain in use if:
|
||||
//
|
||||
// -- The value is not a pointer (e.g., int32, struct)
|
||||
// -- The value is not referenced by the method after passing the pointer to Windows
|
||||
//
|
||||
// See http://golang.org/doc/go1.3.
|
||||
//===========================================================================================================
|
||||
|
||||
var (
|
||||
kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo")
|
||||
setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo")
|
||||
setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition")
|
||||
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
|
||||
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
|
||||
setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize")
|
||||
scrollConsoleScreenBufferProc = kernel32DLL.NewProc("ScrollConsoleScreenBufferA")
|
||||
setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute")
|
||||
setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo")
|
||||
writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW")
|
||||
readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW")
|
||||
waitForSingleObjectProc = kernel32DLL.NewProc("WaitForSingleObject")
|
||||
)
|
||||
|
||||
// Windows Console constants
|
||||
const (
|
||||
// Console modes
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||
ENABLE_PROCESSED_INPUT = 0x0001
|
||||
ENABLE_LINE_INPUT = 0x0002
|
||||
ENABLE_ECHO_INPUT = 0x0004
|
||||
ENABLE_WINDOW_INPUT = 0x0008
|
||||
ENABLE_MOUSE_INPUT = 0x0010
|
||||
ENABLE_INSERT_MODE = 0x0020
|
||||
ENABLE_QUICK_EDIT_MODE = 0x0040
|
||||
ENABLE_EXTENDED_FLAGS = 0x0080
|
||||
|
||||
ENABLE_PROCESSED_OUTPUT = 0x0001
|
||||
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
|
||||
|
||||
// Character attributes
|
||||
// Note:
|
||||
// -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
|
||||
// Clearing all foreground or background colors results in black; setting all creates white.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
|
||||
FOREGROUND_BLUE uint16 = 0x0001
|
||||
FOREGROUND_GREEN uint16 = 0x0002
|
||||
FOREGROUND_RED uint16 = 0x0004
|
||||
FOREGROUND_INTENSITY uint16 = 0x0008
|
||||
FOREGROUND_MASK uint16 = 0x000F
|
||||
|
||||
BACKGROUND_BLUE uint16 = 0x0010
|
||||
BACKGROUND_GREEN uint16 = 0x0020
|
||||
BACKGROUND_RED uint16 = 0x0040
|
||||
BACKGROUND_INTENSITY uint16 = 0x0080
|
||||
BACKGROUND_MASK uint16 = 0x00F0
|
||||
|
||||
COMMON_LVB_MASK uint16 = 0xFF00
|
||||
COMMON_LVB_REVERSE_VIDEO uint16 = 0x4000
|
||||
COMMON_LVB_UNDERSCORE uint16 = 0x8000
|
||||
|
||||
// Input event types
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||
KEY_EVENT = 0x0001
|
||||
MOUSE_EVENT = 0x0002
|
||||
WINDOW_BUFFER_SIZE_EVENT = 0x0004
|
||||
MENU_EVENT = 0x0008
|
||||
FOCUS_EVENT = 0x0010
|
||||
|
||||
// WaitForSingleObject return codes
|
||||
WAIT_ABANDONED = 0x00000080
|
||||
WAIT_FAILED = 0xFFFFFFFF
|
||||
WAIT_SIGNALED = 0x0000000
|
||||
WAIT_TIMEOUT = 0x00000102
|
||||
|
||||
// WaitForSingleObject wait duration
|
||||
WAIT_INFINITE = 0xFFFFFFFF
|
||||
WAIT_ONE_SECOND = 1000
|
||||
WAIT_HALF_SECOND = 500
|
||||
WAIT_QUARTER_SECOND = 250
|
||||
)
|
||||
|
||||
// Windows API Console types
|
||||
// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682101(v=vs.85).aspx for Console specific types (e.g., COORD)
|
||||
// -- See https://msdn.microsoft.com/en-us/library/aa296569(v=vs.60).aspx for comments on alignment
|
||||
type (
|
||||
CHAR_INFO struct {
|
||||
UnicodeChar uint16
|
||||
Attributes uint16
|
||||
}
|
||||
|
||||
CONSOLE_CURSOR_INFO struct {
|
||||
Size uint32
|
||||
Visible int32
|
||||
}
|
||||
|
||||
CONSOLE_SCREEN_BUFFER_INFO struct {
|
||||
Size COORD
|
||||
CursorPosition COORD
|
||||
Attributes uint16
|
||||
Window SMALL_RECT
|
||||
MaximumWindowSize COORD
|
||||
}
|
||||
|
||||
COORD struct {
|
||||
X int16
|
||||
Y int16
|
||||
}
|
||||
|
||||
SMALL_RECT struct {
|
||||
Left int16
|
||||
Top int16
|
||||
Right int16
|
||||
Bottom int16
|
||||
}
|
||||
|
||||
// INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||
INPUT_RECORD struct {
|
||||
EventType uint16
|
||||
KeyEvent KEY_EVENT_RECORD
|
||||
}
|
||||
|
||||
KEY_EVENT_RECORD struct {
|
||||
KeyDown int32
|
||||
RepeatCount uint16
|
||||
VirtualKeyCode uint16
|
||||
VirtualScanCode uint16
|
||||
UnicodeChar uint16
|
||||
ControlKeyState uint32
|
||||
}
|
||||
|
||||
WINDOW_BUFFER_SIZE struct {
|
||||
Size COORD
|
||||
}
|
||||
)
|
||||
|
||||
// boolToBOOL converts a Go bool into a Windows int32.
|
||||
func boolToBOOL(f bool) int32 {
|
||||
if f {
|
||||
return int32(1)
|
||||
} else {
|
||||
return int32(0)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConsoleCursorInfo retrieves information about the size and visiblity of the console cursor.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683163(v=vs.85).aspx.
|
||||
func GetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
|
||||
r1, r2, err := getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// SetConsoleCursorInfo sets the size and visiblity of the console cursor.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx.
|
||||
func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
|
||||
r1, r2, err := setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// SetConsoleCursorPosition location of the console cursor.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx.
|
||||
func SetConsoleCursorPosition(handle uintptr, coord COORD) error {
|
||||
r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord))
|
||||
use(coord)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// GetConsoleMode gets the console mode for given file descriptor
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx.
|
||||
func GetConsoleMode(handle uintptr) (mode uint32, err error) {
|
||||
err = syscall.GetConsoleMode(syscall.Handle(handle), &mode)
|
||||
return mode, err
|
||||
}
|
||||
|
||||
// SetConsoleMode sets the console mode for given file descriptor
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||
func SetConsoleMode(handle uintptr, mode uint32) error {
|
||||
r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0)
|
||||
use(mode)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx.
|
||||
func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
|
||||
info := CONSOLE_SCREEN_BUFFER_INFO{}
|
||||
err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error {
|
||||
r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char)))
|
||||
use(scrollRect)
|
||||
use(clipRect)
|
||||
use(destOrigin)
|
||||
use(char)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// SetConsoleScreenBufferSize sets the size of the console screen buffer.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx.
|
||||
func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error {
|
||||
r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord))
|
||||
use(coord)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// SetConsoleTextAttribute sets the attributes of characters written to the
|
||||
// console screen buffer by the WriteFile or WriteConsole function.
|
||||
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
|
||||
func SetConsoleTextAttribute(handle uintptr, attribute uint16) error {
|
||||
r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)
|
||||
use(attribute)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// SetConsoleWindowInfo sets the size and position of the console screen buffer's window.
|
||||
// Note that the size and location must be within and no larger than the backing console screen buffer.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx.
|
||||
func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error {
|
||||
r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect)))
|
||||
use(isAbsolute)
|
||||
use(rect)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// WriteConsoleOutput writes the CHAR_INFOs from the provided buffer to the active console buffer.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx.
|
||||
func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error {
|
||||
r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))
|
||||
use(buffer)
|
||||
use(bufferSize)
|
||||
use(bufferCoord)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// ReadConsoleInput reads (and removes) data from the console input buffer.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx.
|
||||
func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error {
|
||||
r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count)))
|
||||
use(buffer)
|
||||
return checkError(r1, r2, err)
|
||||
}
|
||||
|
||||
// WaitForSingleObject waits for the passed handle to be signaled.
|
||||
// It returns true if the handle was signaled; false otherwise.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx.
|
||||
func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) {
|
||||
r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(uint32(msWait)))
|
||||
switch r1 {
|
||||
case WAIT_ABANDONED, WAIT_TIMEOUT:
|
||||
return false, nil
|
||||
case WAIT_SIGNALED:
|
||||
return true, nil
|
||||
}
|
||||
use(msWait)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// String helpers
|
||||
func (info CONSOLE_SCREEN_BUFFER_INFO) String() string {
|
||||
return fmt.Sprintf("Size(%v) Cursor(%v) Window(%v) Max(%v)", info.Size, info.CursorPosition, info.Window, info.MaximumWindowSize)
|
||||
}
|
||||
|
||||
func (coord COORD) String() string {
|
||||
return fmt.Sprintf("%v,%v", coord.X, coord.Y)
|
||||
}
|
||||
|
||||
func (rect SMALL_RECT) String() string {
|
||||
return fmt.Sprintf("(%v,%v),(%v,%v)", rect.Left, rect.Top, rect.Right, rect.Bottom)
|
||||
}
|
||||
|
||||
// checkError evaluates the results of a Windows API call and returns the error if it failed.
|
||||
func checkError(r1, r2 uintptr, err error) error {
|
||||
// Windows APIs return non-zero to indicate success
|
||||
if r1 != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the error if provided, otherwise default to EINVAL
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
// coordToPointer converts a COORD into a uintptr (by fooling the type system).
|
||||
func coordToPointer(c COORD) uintptr {
|
||||
// Note: This code assumes the two SHORTs are correctly laid out; the "cast" to uint32 is just to get a pointer to pass.
|
||||
return uintptr(*((*uint32)(unsafe.Pointer(&c))))
|
||||
}
|
||||
|
||||
// use is a no-op, but the compiler cannot see that it is.
|
||||
// Calling use(p) ensures that p is kept live until that point.
|
||||
func use(p interface{}) {}
|
@ -0,0 +1,100 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
import "github.com/Azure/go-ansiterm"
|
||||
|
||||
const (
|
||||
FOREGROUND_COLOR_MASK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
|
||||
BACKGROUND_COLOR_MASK = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
|
||||
)
|
||||
|
||||
// collectAnsiIntoWindowsAttributes modifies the passed Windows text mode flags to reflect the
|
||||
// request represented by the passed ANSI mode.
|
||||
func collectAnsiIntoWindowsAttributes(windowsMode uint16, inverted bool, baseMode uint16, ansiMode int16) (uint16, bool) {
|
||||
switch ansiMode {
|
||||
|
||||
// Mode styles
|
||||
case ansiterm.ANSI_SGR_BOLD:
|
||||
windowsMode = windowsMode | FOREGROUND_INTENSITY
|
||||
|
||||
case ansiterm.ANSI_SGR_DIM, ansiterm.ANSI_SGR_BOLD_DIM_OFF:
|
||||
windowsMode &^= FOREGROUND_INTENSITY
|
||||
|
||||
case ansiterm.ANSI_SGR_UNDERLINE:
|
||||
windowsMode = windowsMode | COMMON_LVB_UNDERSCORE
|
||||
|
||||
case ansiterm.ANSI_SGR_REVERSE:
|
||||
inverted = true
|
||||
|
||||
case ansiterm.ANSI_SGR_REVERSE_OFF:
|
||||
inverted = false
|
||||
|
||||
case ansiterm.ANSI_SGR_UNDERLINE_OFF:
|
||||
windowsMode &^= COMMON_LVB_UNDERSCORE
|
||||
|
||||
// Foreground colors
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_DEFAULT:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_MASK) | (baseMode & FOREGROUND_MASK)
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_BLACK:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK)
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_RED:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_GREEN:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_YELLOW:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_BLUE:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_BLUE
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_MAGENTA:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_BLUE
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_CYAN:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE
|
||||
|
||||
case ansiterm.ANSI_SGR_FOREGROUND_WHITE:
|
||||
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
|
||||
|
||||
// Background colors
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_DEFAULT:
|
||||
// Black with no intensity
|
||||
windowsMode = (windowsMode &^ BACKGROUND_MASK) | (baseMode & BACKGROUND_MASK)
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_BLACK:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK)
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_RED:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_GREEN:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_YELLOW:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_BLUE:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_BLUE
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_MAGENTA:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_BLUE
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_CYAN:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE
|
||||
|
||||
case ansiterm.ANSI_SGR_BACKGROUND_WHITE:
|
||||
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
|
||||
}
|
||||
|
||||
return windowsMode, inverted
|
||||
}
|
||||
|
||||
// invertAttributes inverts the foreground and background colors of a Windows attributes value
|
||||
func invertAttributes(windowsMode uint16) uint16 {
|
||||
return (COMMON_LVB_MASK & windowsMode) | ((FOREGROUND_MASK & windowsMode) << 4) | ((BACKGROUND_MASK & windowsMode) >> 4)
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
const (
|
||||
horizontal = iota
|
||||
vertical
|
||||
)
|
||||
|
||||
func (h *windowsAnsiEventHandler) getCursorWindow(info *CONSOLE_SCREEN_BUFFER_INFO) SMALL_RECT {
|
||||
if h.originMode {
|
||||
sr := h.effectiveSr(info.Window)
|
||||
return SMALL_RECT{
|
||||
Top: sr.top,
|
||||
Bottom: sr.bottom,
|
||||
Left: 0,
|
||||
Right: info.Size.X - 1,
|
||||
}
|
||||
} else {
|
||||
return SMALL_RECT{
|
||||
Top: info.Window.Top,
|
||||
Bottom: info.Window.Bottom,
|
||||
Left: 0,
|
||||
Right: info.Size.X - 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setCursorPosition sets the cursor to the specified position, bounded to the screen size
|
||||
func (h *windowsAnsiEventHandler) setCursorPosition(position COORD, window SMALL_RECT) error {
|
||||
position.X = ensureInRange(position.X, window.Left, window.Right)
|
||||
position.Y = ensureInRange(position.Y, window.Top, window.Bottom)
|
||||
err := SetConsoleCursorPosition(h.fd, position)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("Cursor position set: (%d, %d)", position.X, position.Y)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) moveCursorVertical(param int) error {
|
||||
return h.moveCursor(vertical, param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) moveCursorHorizontal(param int) error {
|
||||
return h.moveCursor(horizontal, param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) moveCursor(moveMode int, param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
position := info.CursorPosition
|
||||
switch moveMode {
|
||||
case horizontal:
|
||||
position.X += int16(param)
|
||||
case vertical:
|
||||
position.Y += int16(param)
|
||||
}
|
||||
|
||||
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) moveCursorLine(param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
position := info.CursorPosition
|
||||
position.X = 0
|
||||
position.Y += int16(param)
|
||||
|
||||
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) moveCursorColumn(param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
position := info.CursorPosition
|
||||
position.X = int16(param) - 1
|
||||
|
||||
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
import "github.com/Azure/go-ansiterm"
|
||||
|
||||
func (h *windowsAnsiEventHandler) clearRange(attributes uint16, fromCoord COORD, toCoord COORD) error {
|
||||
// Ignore an invalid (negative area) request
|
||||
if toCoord.Y < fromCoord.Y {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
var coordStart = COORD{}
|
||||
var coordEnd = COORD{}
|
||||
|
||||
xCurrent, yCurrent := fromCoord.X, fromCoord.Y
|
||||
xEnd, yEnd := toCoord.X, toCoord.Y
|
||||
|
||||
// Clear any partial initial line
|
||||
if xCurrent > 0 {
|
||||
coordStart.X, coordStart.Y = xCurrent, yCurrent
|
||||
coordEnd.X, coordEnd.Y = xEnd, yCurrent
|
||||
|
||||
err = h.clearRect(attributes, coordStart, coordEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xCurrent = 0
|
||||
yCurrent += 1
|
||||
}
|
||||
|
||||
// Clear intervening rectangular section
|
||||
if yCurrent < yEnd {
|
||||
coordStart.X, coordStart.Y = xCurrent, yCurrent
|
||||
coordEnd.X, coordEnd.Y = xEnd, yEnd-1
|
||||
|
||||
err = h.clearRect(attributes, coordStart, coordEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xCurrent = 0
|
||||
yCurrent = yEnd
|
||||
}
|
||||
|
||||
// Clear remaining partial ending line
|
||||
coordStart.X, coordStart.Y = xCurrent, yCurrent
|
||||
coordEnd.X, coordEnd.Y = xEnd, yEnd
|
||||
|
||||
err = h.clearRect(attributes, coordStart, coordEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD, toCoord COORD) error {
|
||||
region := SMALL_RECT{Top: fromCoord.Y, Left: fromCoord.X, Bottom: toCoord.Y, Right: toCoord.X}
|
||||
width := toCoord.X - fromCoord.X + 1
|
||||
height := toCoord.Y - fromCoord.Y + 1
|
||||
size := uint32(width) * uint32(height)
|
||||
|
||||
if size <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := make([]CHAR_INFO, size)
|
||||
|
||||
char := CHAR_INFO{ansiterm.FILL_CHARACTER, attributes}
|
||||
for i := 0; i < int(size); i++ {
|
||||
buffer[i] = char
|
||||
}
|
||||
|
||||
err := WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, ®ion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
// effectiveSr gets the current effective scroll region in buffer coordinates
|
||||
func (h *windowsAnsiEventHandler) effectiveSr(window SMALL_RECT) scrollRegion {
|
||||
top := addInRange(window.Top, h.sr.top, window.Top, window.Bottom)
|
||||
bottom := addInRange(window.Top, h.sr.bottom, window.Top, window.Bottom)
|
||||
if top >= bottom {
|
||||
top = window.Top
|
||||
bottom = window.Bottom
|
||||
}
|
||||
return scrollRegion{top: top, bottom: bottom}
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) scrollUp(param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sr := h.effectiveSr(info.Window)
|
||||
return h.scroll(param, sr, info)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) scrollDown(param int) error {
|
||||
return h.scrollUp(-param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) deleteLines(param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
start := info.CursorPosition.Y
|
||||
sr := h.effectiveSr(info.Window)
|
||||
// Lines cannot be inserted or deleted outside the scrolling region.
|
||||
if start >= sr.top && start <= sr.bottom {
|
||||
sr.top = start
|
||||
return h.scroll(param, sr, info)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) insertLines(param int) error {
|
||||
return h.deleteLines(-param)
|
||||
}
|
||||
|
||||
// scroll scrolls the provided scroll region by param lines. The scroll region is in buffer coordinates.
|
||||
func (h *windowsAnsiEventHandler) scroll(param int, sr scrollRegion, info *CONSOLE_SCREEN_BUFFER_INFO) error {
|
||||
logger.Infof("scroll: scrollTop: %d, scrollBottom: %d", sr.top, sr.bottom)
|
||||
logger.Infof("scroll: windowTop: %d, windowBottom: %d", info.Window.Top, info.Window.Bottom)
|
||||
|
||||
// Copy from and clip to the scroll region (full buffer width)
|
||||
scrollRect := SMALL_RECT{
|
||||
Top: sr.top,
|
||||
Bottom: sr.bottom,
|
||||
Left: 0,
|
||||
Right: info.Size.X - 1,
|
||||
}
|
||||
|
||||
// Origin to which area should be copied
|
||||
destOrigin := COORD{
|
||||
X: 0,
|
||||
Y: sr.top - int16(param),
|
||||
}
|
||||
|
||||
char := CHAR_INFO{
|
||||
UnicodeChar: ' ',
|
||||
Attributes: h.attributes,
|
||||
}
|
||||
|
||||
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) deleteCharacters(param int) error {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return h.scrollLine(param, info.CursorPosition, info)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) insertCharacters(param int) error {
|
||||
return h.deleteCharacters(-param)
|
||||
}
|
||||
|
||||
// scrollLine scrolls a line horizontally starting at the provided position by a number of columns.
|
||||
func (h *windowsAnsiEventHandler) scrollLine(columns int, position COORD, info *CONSOLE_SCREEN_BUFFER_INFO) error {
|
||||
// Copy from and clip to the scroll region (full buffer width)
|
||||
scrollRect := SMALL_RECT{
|
||||
Top: position.Y,
|
||||
Bottom: position.Y,
|
||||
Left: position.X,
|
||||
Right: info.Size.X - 1,
|
||||
}
|
||||
|
||||
// Origin to which area should be copied
|
||||
destOrigin := COORD{
|
||||
X: position.X - int16(columns),
|
||||
Y: position.Y,
|
||||
}
|
||||
|
||||
char := CHAR_INFO{
|
||||
UnicodeChar: ' ',
|
||||
Attributes: h.attributes,
|
||||
}
|
||||
|
||||
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
// AddInRange increments a value by the passed quantity while ensuring the values
|
||||
// always remain within the supplied min / max range.
|
||||
func addInRange(n int16, increment int16, min int16, max int16) int16 {
|
||||
return ensureInRange(n+increment, min, max)
|
||||
}
|
@ -0,0 +1,726 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/Azure/go-ansiterm"
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
var logger *logrus.Logger
|
||||
|
||||
type windowsAnsiEventHandler struct {
|
||||
fd uintptr
|
||||
file *os.File
|
||||
infoReset *CONSOLE_SCREEN_BUFFER_INFO
|
||||
sr scrollRegion
|
||||
buffer bytes.Buffer
|
||||
attributes uint16
|
||||
inverted bool
|
||||
wrapNext bool
|
||||
drewMarginByte bool
|
||||
originMode bool
|
||||
marginByte byte
|
||||
curInfo *CONSOLE_SCREEN_BUFFER_INFO
|
||||
curPos COORD
|
||||
}
|
||||
|
||||
func CreateWinEventHandler(fd uintptr, file *os.File) ansiterm.AnsiEventHandler {
|
||||
logFile := ioutil.Discard
|
||||
|
||||
if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" {
|
||||
logFile, _ = os.Create("winEventHandler.log")
|
||||
}
|
||||
|
||||
logger = &logrus.Logger{
|
||||
Out: logFile,
|
||||
Formatter: new(logrus.TextFormatter),
|
||||
Level: logrus.DebugLevel,
|
||||
}
|
||||
|
||||
infoReset, err := GetConsoleScreenBufferInfo(fd)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &windowsAnsiEventHandler{
|
||||
fd: fd,
|
||||
file: file,
|
||||
infoReset: infoReset,
|
||||
attributes: infoReset.Attributes,
|
||||
}
|
||||
}
|
||||
|
||||
type scrollRegion struct {
|
||||
top int16
|
||||
bottom int16
|
||||
}
|
||||
|
||||
// simulateLF simulates a LF or CR+LF by scrolling if necessary to handle the
|
||||
// current cursor position and scroll region settings, in which case it returns
|
||||
// true. If no special handling is necessary, then it does nothing and returns
|
||||
// false.
|
||||
//
|
||||
// In the false case, the caller should ensure that a carriage return
|
||||
// and line feed are inserted or that the text is otherwise wrapped.
|
||||
func (h *windowsAnsiEventHandler) simulateLF(includeCR bool) (bool, error) {
|
||||
if h.wrapNext {
|
||||
if err := h.Flush(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
h.clearWrap()
|
||||
}
|
||||
pos, info, err := h.getCurrentInfo()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
sr := h.effectiveSr(info.Window)
|
||||
if pos.Y == sr.bottom {
|
||||
// Scrolling is necessary. Let Windows automatically scroll if the scrolling region
|
||||
// is the full window.
|
||||
if sr.top == info.Window.Top && sr.bottom == info.Window.Bottom {
|
||||
if includeCR {
|
||||
pos.X = 0
|
||||
h.updatePos(pos)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// A custom scroll region is active. Scroll the window manually to simulate
|
||||
// the LF.
|
||||
if err := h.Flush(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
logger.Info("Simulating LF inside scroll region")
|
||||
if err := h.scrollUp(1); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if includeCR {
|
||||
pos.X = 0
|
||||
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
|
||||
} else if pos.Y < info.Window.Bottom {
|
||||
// Let Windows handle the LF.
|
||||
pos.Y++
|
||||
if includeCR {
|
||||
pos.X = 0
|
||||
}
|
||||
h.updatePos(pos)
|
||||
return false, nil
|
||||
} else {
|
||||
// The cursor is at the bottom of the screen but outside the scroll
|
||||
// region. Skip the LF.
|
||||
logger.Info("Simulating LF outside scroll region")
|
||||
if includeCR {
|
||||
if err := h.Flush(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
pos.X = 0
|
||||
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// executeLF executes a LF without a CR.
|
||||
func (h *windowsAnsiEventHandler) executeLF() error {
|
||||
handled, err := h.simulateLF(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !handled {
|
||||
// Windows LF will reset the cursor column position. Write the LF
|
||||
// and restore the cursor position.
|
||||
pos, _, err := h.getCurrentInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
|
||||
if pos.X != 0 {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("Resetting cursor position for LF without CR")
|
||||
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) Print(b byte) error {
|
||||
if h.wrapNext {
|
||||
h.buffer.WriteByte(h.marginByte)
|
||||
h.clearWrap()
|
||||
if _, err := h.simulateLF(true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pos, info, err := h.getCurrentInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pos.X == info.Size.X-1 {
|
||||
h.wrapNext = true
|
||||
h.marginByte = b
|
||||
} else {
|
||||
pos.X++
|
||||
h.updatePos(pos)
|
||||
h.buffer.WriteByte(b)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) Execute(b byte) error {
|
||||
switch b {
|
||||
case ansiterm.ANSI_TAB:
|
||||
logger.Info("Execute(TAB)")
|
||||
// Move to the next tab stop, but preserve auto-wrap if already set.
|
||||
if !h.wrapNext {
|
||||
pos, info, err := h.getCurrentInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pos.X = (pos.X + 8) - pos.X%8
|
||||
if pos.X >= info.Size.X {
|
||||
pos.X = info.Size.X - 1
|
||||
}
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case ansiterm.ANSI_BEL:
|
||||
h.buffer.WriteByte(ansiterm.ANSI_BEL)
|
||||
return nil
|
||||
|
||||
case ansiterm.ANSI_BACKSPACE:
|
||||
if h.wrapNext {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.clearWrap()
|
||||
}
|
||||
pos, _, err := h.getCurrentInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pos.X > 0 {
|
||||
pos.X--
|
||||
h.updatePos(pos)
|
||||
h.buffer.WriteByte(ansiterm.ANSI_BACKSPACE)
|
||||
}
|
||||
return nil
|
||||
|
||||
case ansiterm.ANSI_VERTICAL_TAB, ansiterm.ANSI_FORM_FEED:
|
||||
// Treat as true LF.
|
||||
return h.executeLF()
|
||||
|
||||
case ansiterm.ANSI_LINE_FEED:
|
||||
// Simulate a CR and LF for now since there is no way in go-ansiterm
|
||||
// to tell if the LF should include CR (and more things break when it's
|
||||
// missing than when it's incorrectly added).
|
||||
handled, err := h.simulateLF(true)
|
||||
if handled || err != nil {
|
||||
return err
|
||||
}
|
||||
return h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
|
||||
|
||||
case ansiterm.ANSI_CARRIAGE_RETURN:
|
||||
if h.wrapNext {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.clearWrap()
|
||||
}
|
||||
pos, _, err := h.getCurrentInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pos.X != 0 {
|
||||
pos.X = 0
|
||||
h.updatePos(pos)
|
||||
h.buffer.WriteByte(ansiterm.ANSI_CARRIAGE_RETURN)
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CUU(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CUU: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.moveCursorVertical(-param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CUD(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CUD: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.moveCursorVertical(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CUF(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CUF: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.moveCursorHorizontal(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CUB(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CUB: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.moveCursorHorizontal(-param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CNL(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CNL: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.moveCursorLine(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CPL(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CPL: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.moveCursorLine(-param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CHA(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CHA: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.moveCursorColumn(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) VPA(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("VPA: [[%d]]", param)
|
||||
h.clearWrap()
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
window := h.getCursorWindow(info)
|
||||
position := info.CursorPosition
|
||||
position.Y = window.Top + int16(param) - 1
|
||||
return h.setCursorPosition(position, window)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) CUP(row int, col int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("CUP: [[%d %d]]", row, col)
|
||||
h.clearWrap()
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
window := h.getCursorWindow(info)
|
||||
position := COORD{window.Left + int16(col) - 1, window.Top + int16(row) - 1}
|
||||
return h.setCursorPosition(position, window)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) HVP(row int, col int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("HVP: [[%d %d]]", row, col)
|
||||
h.clearWrap()
|
||||
return h.CUP(row, col)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) DECTCEM(visible bool) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("DECTCEM: [%v]", []string{strconv.FormatBool(visible)})
|
||||
h.clearWrap()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) DECOM(enable bool) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("DECOM: [%v]", []string{strconv.FormatBool(enable)})
|
||||
h.clearWrap()
|
||||
h.originMode = enable
|
||||
return h.CUP(1, 1)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("DECCOLM: [%v]", []string{strconv.FormatBool(use132)})
|
||||
h.clearWrap()
|
||||
if err := h.ED(2); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetWidth := int16(80)
|
||||
if use132 {
|
||||
targetWidth = 132
|
||||
}
|
||||
if info.Size.X < targetWidth {
|
||||
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
|
||||
logger.Info("set buffer failed:", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
window := info.Window
|
||||
window.Left = 0
|
||||
window.Right = targetWidth - 1
|
||||
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
|
||||
logger.Info("set window failed:", err)
|
||||
return err
|
||||
}
|
||||
if info.Size.X > targetWidth {
|
||||
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
|
||||
logger.Info("set buffer failed:", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return SetConsoleCursorPosition(h.fd, COORD{0, 0})
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) ED(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("ED: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
|
||||
// [J -- Erases from the cursor to the end of the screen, including the cursor position.
|
||||
// [1J -- Erases from the beginning of the screen to the cursor, including the cursor position.
|
||||
// [2J -- Erases the complete display. The cursor does not move.
|
||||
// Notes:
|
||||
// -- Clearing the entire buffer, versus just the Window, works best for Windows Consoles
|
||||
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var start COORD
|
||||
var end COORD
|
||||
|
||||
switch param {
|
||||
case 0:
|
||||
start = info.CursorPosition
|
||||
end = COORD{info.Size.X - 1, info.Size.Y - 1}
|
||||
|
||||
case 1:
|
||||
start = COORD{0, 0}
|
||||
end = info.CursorPosition
|
||||
|
||||
case 2:
|
||||
start = COORD{0, 0}
|
||||
end = COORD{info.Size.X - 1, info.Size.Y - 1}
|
||||
}
|
||||
|
||||
err = h.clearRange(h.attributes, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the whole buffer was cleared, move the window to the top while preserving
|
||||
// the window-relative cursor position.
|
||||
if param == 2 {
|
||||
pos := info.CursorPosition
|
||||
window := info.Window
|
||||
pos.Y -= window.Top
|
||||
window.Bottom -= window.Top
|
||||
window.Top = 0
|
||||
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) EL(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("EL: [%v]", strconv.Itoa(param))
|
||||
h.clearWrap()
|
||||
|
||||
// [K -- Erases from the cursor to the end of the line, including the cursor position.
|
||||
// [1K -- Erases from the beginning of the line to the cursor, including the cursor position.
|
||||
// [2K -- Erases the complete line.
|
||||
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var start COORD
|
||||
var end COORD
|
||||
|
||||
switch param {
|
||||
case 0:
|
||||
start = info.CursorPosition
|
||||
end = COORD{info.Size.X, info.CursorPosition.Y}
|
||||
|
||||
case 1:
|
||||
start = COORD{0, info.CursorPosition.Y}
|
||||
end = info.CursorPosition
|
||||
|
||||
case 2:
|
||||
start = COORD{0, info.CursorPosition.Y}
|
||||
end = COORD{info.Size.X, info.CursorPosition.Y}
|
||||
}
|
||||
|
||||
err = h.clearRange(h.attributes, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) IL(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("IL: [%v]", strconv.Itoa(param))
|
||||
h.clearWrap()
|
||||
return h.insertLines(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) DL(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("DL: [%v]", strconv.Itoa(param))
|
||||
h.clearWrap()
|
||||
return h.deleteLines(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) ICH(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("ICH: [%v]", strconv.Itoa(param))
|
||||
h.clearWrap()
|
||||
return h.insertCharacters(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) DCH(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("DCH: [%v]", strconv.Itoa(param))
|
||||
h.clearWrap()
|
||||
return h.deleteCharacters(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) SGR(params []int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
strings := []string{}
|
||||
for _, v := range params {
|
||||
strings = append(strings, strconv.Itoa(v))
|
||||
}
|
||||
|
||||
logger.Infof("SGR: [%v]", strings)
|
||||
|
||||
if len(params) <= 0 {
|
||||
h.attributes = h.infoReset.Attributes
|
||||
h.inverted = false
|
||||
} else {
|
||||
for _, attr := range params {
|
||||
|
||||
if attr == ansiterm.ANSI_SGR_RESET {
|
||||
h.attributes = h.infoReset.Attributes
|
||||
h.inverted = false
|
||||
continue
|
||||
}
|
||||
|
||||
h.attributes, h.inverted = collectAnsiIntoWindowsAttributes(h.attributes, h.inverted, h.infoReset.Attributes, int16(attr))
|
||||
}
|
||||
}
|
||||
|
||||
attributes := h.attributes
|
||||
if h.inverted {
|
||||
attributes = invertAttributes(attributes)
|
||||
}
|
||||
err := SetConsoleTextAttribute(h.fd, attributes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) SU(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("SU: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.scrollUp(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) SD(param int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("SD: [%v]", []string{strconv.Itoa(param)})
|
||||
h.clearWrap()
|
||||
return h.scrollDown(param)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) DA(params []string) error {
|
||||
logger.Infof("DA: [%v]", params)
|
||||
// DA cannot be implemented because it must send data on the VT100 input stream,
|
||||
// which is not available to go-ansiterm.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) DECSTBM(top int, bottom int) error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("DECSTBM: [%d, %d]", top, bottom)
|
||||
|
||||
// Windows is 0 indexed, Linux is 1 indexed
|
||||
h.sr.top = int16(top - 1)
|
||||
h.sr.bottom = int16(bottom - 1)
|
||||
|
||||
// This command also moves the cursor to the origin.
|
||||
h.clearWrap()
|
||||
return h.CUP(1, 1)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) RI() error {
|
||||
if err := h.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("RI: []")
|
||||
h.clearWrap()
|
||||
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sr := h.effectiveSr(info.Window)
|
||||
if info.CursorPosition.Y == sr.top {
|
||||
return h.scrollDown(1)
|
||||
}
|
||||
|
||||
return h.moveCursorVertical(-1)
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) IND() error {
|
||||
logger.Info("IND: []")
|
||||
return h.executeLF()
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) Flush() error {
|
||||
h.curInfo = nil
|
||||
if h.buffer.Len() > 0 {
|
||||
logger.Infof("Flush: [%s]", h.buffer.Bytes())
|
||||
if _, err := h.buffer.WriteTo(h.file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if h.wrapNext && !h.drewMarginByte {
|
||||
logger.Infof("Flush: drawing margin byte '%c'", h.marginByte)
|
||||
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
charInfo := []CHAR_INFO{{UnicodeChar: uint16(h.marginByte), Attributes: info.Attributes}}
|
||||
size := COORD{1, 1}
|
||||
position := COORD{0, 0}
|
||||
region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y}
|
||||
if err := WriteConsoleOutput(h.fd, charInfo, size, position, ®ion); err != nil {
|
||||
return err
|
||||
}
|
||||
h.drewMarginByte = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cacheConsoleInfo ensures that the current console screen information has been queried
|
||||
// since the last call to Flush(). It must be called before accessing h.curInfo or h.curPos.
|
||||
func (h *windowsAnsiEventHandler) getCurrentInfo() (COORD, *CONSOLE_SCREEN_BUFFER_INFO, error) {
|
||||
if h.curInfo == nil {
|
||||
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||
if err != nil {
|
||||
return COORD{}, nil, err
|
||||
}
|
||||
h.curInfo = info
|
||||
h.curPos = info.CursorPosition
|
||||
}
|
||||
return h.curPos, h.curInfo, nil
|
||||
}
|
||||
|
||||
func (h *windowsAnsiEventHandler) updatePos(pos COORD) {
|
||||
if h.curInfo == nil {
|
||||
panic("failed to call getCurrentInfo before calling updatePos")
|
||||
}
|
||||
h.curPos = pos
|
||||
}
|
||||
|
||||
// clearWrap clears the state where the cursor is in the margin
|
||||
// waiting for the next character before wrapping the line. This must
|
||||
// be done before most operations that act on the cursor.
|
||||
func (h *windowsAnsiEventHandler) clearWrap() {
|
||||
h.wrapNext = false
|
||||
h.drewMarginByte = false
|
||||
}
|
@ -0,0 +1 @@
|
||||
*.exe
|
@ -0,0 +1,22 @@
|
||||
# go-winio
|
||||
|
||||
This repository contains utilities for efficiently performing Win32 IO operations in
|
||||
Go. Currently, this is focused on accessing named pipes and other file handles, and
|
||||
for using named pipes as a net transport.
|
||||
|
||||
This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go
|
||||
to reuse the thread to schedule another goroutine. This limits support to Windows Vista and
|
||||
newer operating systems. This is similar to the implementation of network sockets in Go's net
|
||||
package.
|
||||
|
||||
Please see the LICENSE file for licensing information.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of
|
||||
Conduct](https://opensource.microsoft.com/codeofconduct/). For more information
|
||||
see the [Code of Conduct
|
||||
FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact
|
||||
[opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional
|
||||
questions or comments.
|
||||
|
||||
Thanks to natefinch for the inspiration for this library. See https://github.com/natefinch/npipe
|
||||
for another named pipe implementation.
|
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,344 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tar implements access to tar archives.
|
||||
// It aims to cover most of the variations, including those produced
|
||||
// by GNU and BSD tars.
|
||||
//
|
||||
// References:
|
||||
// http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5
|
||||
// http://www.gnu.org/software/tar/manual/html_node/Standard.html
|
||||
// http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html
|
||||
package tar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
blockSize = 512
|
||||
|
||||
// Types
|
||||
TypeReg = '0' // regular file
|
||||
TypeRegA = '\x00' // regular file
|
||||
TypeLink = '1' // hard link
|
||||
TypeSymlink = '2' // symbolic link
|
||||
TypeChar = '3' // character device node
|
||||
TypeBlock = '4' // block device node
|
||||
TypeDir = '5' // directory
|
||||
TypeFifo = '6' // fifo node
|
||||
TypeCont = '7' // reserved
|
||||
TypeXHeader = 'x' // extended header
|
||||
TypeXGlobalHeader = 'g' // global extended header
|
||||
TypeGNULongName = 'L' // Next file has a long name
|
||||
TypeGNULongLink = 'K' // Next file symlinks to a file w/ a long name
|
||||
TypeGNUSparse = 'S' // sparse file
|
||||
)
|
||||
|
||||
// A Header represents a single header in a tar archive.
|
||||
// Some fields may not be populated.
|
||||
type Header struct {
|
||||
Name string // name of header file entry
|
||||
Mode int64 // permission and mode bits
|
||||
Uid int // user id of owner
|
||||
Gid int // group id of owner
|
||||
Size int64 // length in bytes
|
||||
ModTime time.Time // modified time
|
||||
Typeflag byte // type of header entry
|
||||
Linkname string // target name of link
|
||||
Uname string // user name of owner
|
||||
Gname string // group name of owner
|
||||
Devmajor int64 // major number of character or block device
|
||||
Devminor int64 // minor number of character or block device
|
||||
AccessTime time.Time // access time
|
||||
ChangeTime time.Time // status change time
|
||||
CreationTime time.Time // creation time
|
||||
Xattrs map[string]string
|
||||
Winheaders map[string]string
|
||||
}
|
||||
|
||||
// File name constants from the tar spec.
|
||||
const (
|
||||
fileNameSize = 100 // Maximum number of bytes in a standard tar name.
|
||||
fileNamePrefixSize = 155 // Maximum number of ustar extension bytes.
|
||||
)
|
||||
|
||||
// FileInfo returns an os.FileInfo for the Header.
|
||||
func (h *Header) FileInfo() os.FileInfo {
|
||||
return headerFileInfo{h}
|
||||
}
|
||||
|
||||
// headerFileInfo implements os.FileInfo.
|
||||
type headerFileInfo struct {
|
||||
h *Header
|
||||
}
|
||||
|
||||
func (fi headerFileInfo) Size() int64 { return fi.h.Size }
|
||||
func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
|
||||
func (fi headerFileInfo) ModTime() time.Time { return fi.h.ModTime }
|
||||
func (fi headerFileInfo) Sys() interface{} { return fi.h }
|
||||
|
||||
// Name returns the base name of the file.
|
||||
func (fi headerFileInfo) Name() string {
|
||||
if fi.IsDir() {
|
||||
return path.Base(path.Clean(fi.h.Name))
|
||||
}
|
||||
return path.Base(fi.h.Name)
|
||||
}
|
||||
|
||||
// Mode returns the permission and mode bits for the headerFileInfo.
|
||||
func (fi headerFileInfo) Mode() (mode os.FileMode) {
|
||||
// Set file permission bits.
|
||||
mode = os.FileMode(fi.h.Mode).Perm()
|
||||
|
||||
// Set setuid, setgid and sticky bits.
|
||||
if fi.h.Mode&c_ISUID != 0 {
|
||||
// setuid
|
||||
mode |= os.ModeSetuid
|
||||
}
|
||||
if fi.h.Mode&c_ISGID != 0 {
|
||||
// setgid
|
||||
mode |= os.ModeSetgid
|
||||
}
|
||||
if fi.h.Mode&c_ISVTX != 0 {
|
||||
// sticky
|
||||
mode |= os.ModeSticky
|
||||
}
|
||||
|
||||
// Set file mode bits.
|
||||
// clear perm, setuid, setgid and sticky bits.
|
||||
m := os.FileMode(fi.h.Mode) &^ 07777
|
||||
if m == c_ISDIR {
|
||||
// directory
|
||||
mode |= os.ModeDir
|
||||
}
|
||||
if m == c_ISFIFO {
|
||||
// named pipe (FIFO)
|
||||
mode |= os.ModeNamedPipe
|
||||
}
|
||||
if m == c_ISLNK {
|
||||
// symbolic link
|
||||
mode |= os.ModeSymlink
|
||||
}
|
||||
if m == c_ISBLK {
|
||||
// device file
|
||||
mode |= os.ModeDevice
|
||||
}
|
||||
if m == c_ISCHR {
|
||||
// Unix character device
|
||||
mode |= os.ModeDevice
|
||||
mode |= os.ModeCharDevice
|
||||
}
|
||||
if m == c_ISSOCK {
|
||||
// Unix domain socket
|
||||
mode |= os.ModeSocket
|
||||
}
|
||||
|
||||
switch fi.h.Typeflag {
|
||||
case TypeSymlink:
|
||||
// symbolic link
|
||||
mode |= os.ModeSymlink
|
||||
case TypeChar:
|
||||
// character device node
|
||||
mode |= os.ModeDevice
|
||||
mode |= os.ModeCharDevice
|
||||
case TypeBlock:
|
||||
// block device node
|
||||
mode |= os.ModeDevice
|
||||
case TypeDir:
|
||||
// directory
|
||||
mode |= os.ModeDir
|
||||
case TypeFifo:
|
||||
// fifo node
|
||||
mode |= os.ModeNamedPipe
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
// sysStat, if non-nil, populates h from system-dependent fields of fi.
|
||||
var sysStat func(fi os.FileInfo, h *Header) error
|
||||
|
||||
// Mode constants from the tar spec.
|
||||
const (
|
||||
c_ISUID = 04000 // Set uid
|
||||
c_ISGID = 02000 // Set gid
|
||||
c_ISVTX = 01000 // Save text (sticky bit)
|
||||
c_ISDIR = 040000 // Directory
|
||||
c_ISFIFO = 010000 // FIFO
|
||||
c_ISREG = 0100000 // Regular file
|
||||
c_ISLNK = 0120000 // Symbolic link
|
||||
c_ISBLK = 060000 // Block special file
|
||||
c_ISCHR = 020000 // Character special file
|
||||
c_ISSOCK = 0140000 // Socket
|
||||
)
|
||||
|
||||
// Keywords for the PAX Extended Header
|
||||
const (
|
||||
paxAtime = "atime"
|
||||
paxCharset = "charset"
|
||||
paxComment = "comment"
|
||||
paxCtime = "ctime" // please note that ctime is not a valid pax header.
|
||||
paxCreationTime = "LIBARCHIVE.creationtime"
|
||||
paxGid = "gid"
|
||||
paxGname = "gname"
|
||||
paxLinkpath = "linkpath"
|
||||
paxMtime = "mtime"
|
||||
paxPath = "path"
|
||||
paxSize = "size"
|
||||
paxUid = "uid"
|
||||
paxUname = "uname"
|
||||
paxXattr = "SCHILY.xattr."
|
||||
paxWindows = "MSWINDOWS."
|
||||
paxNone = ""
|
||||
)
|
||||
|
||||
// FileInfoHeader creates a partially-populated Header from fi.
|
||||
// If fi describes a symlink, FileInfoHeader records link as the link target.
|
||||
// If fi describes a directory, a slash is appended to the name.
|
||||
// Because os.FileInfo's Name method returns only the base name of
|
||||
// the file it describes, it may be necessary to modify the Name field
|
||||
// of the returned header to provide the full path name of the file.
|
||||
func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {
|
||||
if fi == nil {
|
||||
return nil, errors.New("tar: FileInfo is nil")
|
||||
}
|
||||
fm := fi.Mode()
|
||||
h := &Header{
|
||||
Name: fi.Name(),
|
||||
ModTime: fi.ModTime(),
|
||||
Mode: int64(fm.Perm()), // or'd with c_IS* constants later
|
||||
}
|
||||
switch {
|
||||
case fm.IsRegular():
|
||||
h.Mode |= c_ISREG
|
||||
h.Typeflag = TypeReg
|
||||
h.Size = fi.Size()
|
||||
case fi.IsDir():
|
||||
h.Typeflag = TypeDir
|
||||
h.Mode |= c_ISDIR
|
||||
h.Name += "/"
|
||||
case fm&os.ModeSymlink != 0:
|
||||
h.Typeflag = TypeSymlink
|
||||
h.Mode |= c_ISLNK
|
||||
h.Linkname = link
|
||||
case fm&os.ModeDevice != 0:
|
||||
if fm&os.ModeCharDevice != 0 {
|
||||
h.Mode |= c_ISCHR
|
||||
h.Typeflag = TypeChar
|
||||
} else {
|
||||
h.Mode |= c_ISBLK
|
||||
h.Typeflag = TypeBlock
|
||||
}
|
||||
case fm&os.ModeNamedPipe != 0:
|
||||
h.Typeflag = TypeFifo
|
||||
h.Mode |= c_ISFIFO
|
||||
case fm&os.ModeSocket != 0:
|
||||
h.Mode |= c_ISSOCK
|
||||
default:
|
||||
return nil, fmt.Errorf("archive/tar: unknown file mode %v", fm)
|
||||
}
|
||||
if fm&os.ModeSetuid != 0 {
|
||||
h.Mode |= c_ISUID
|
||||
}
|
||||
if fm&os.ModeSetgid != 0 {
|
||||
h.Mode |= c_ISGID
|
||||
}
|
||||
if fm&os.ModeSticky != 0 {
|
||||
h.Mode |= c_ISVTX
|
||||
}
|
||||
// If possible, populate additional fields from OS-specific
|
||||
// FileInfo fields.
|
||||
if sys, ok := fi.Sys().(*Header); ok {
|
||||
// This FileInfo came from a Header (not the OS). Use the
|
||||
// original Header to populate all remaining fields.
|
||||
h.Uid = sys.Uid
|
||||
h.Gid = sys.Gid
|
||||
h.Uname = sys.Uname
|
||||
h.Gname = sys.Gname
|
||||
h.AccessTime = sys.AccessTime
|
||||
h.ChangeTime = sys.ChangeTime
|
||||
if sys.Xattrs != nil {
|
||||
h.Xattrs = make(map[string]string)
|
||||
for k, v := range sys.Xattrs {
|
||||
h.Xattrs[k] = v
|
||||
}
|
||||
}
|
||||
if sys.Typeflag == TypeLink {
|
||||
// hard link
|
||||
h.Typeflag = TypeLink
|
||||
h.Size = 0
|
||||
h.Linkname = sys.Linkname
|
||||
}
|
||||
}
|
||||
if sysStat != nil {
|
||||
return h, sysStat(fi, h)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
var zeroBlock = make([]byte, blockSize)
|
||||
|
||||
// POSIX specifies a sum of the unsigned byte values, but the Sun tar uses signed byte values.
|
||||
// We compute and return both.
|
||||
func checksum(header []byte) (unsigned int64, signed int64) {
|
||||
for i := 0; i < len(header); i++ {
|
||||
if i == 148 {
|
||||
// The chksum field (header[148:156]) is special: it should be treated as space bytes.
|
||||
unsigned += ' ' * 8
|
||||
signed += ' ' * 8
|
||||
i += 7
|
||||
continue
|
||||
}
|
||||
unsigned += int64(header[i])
|
||||
signed += int64(int8(header[i]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type slicer []byte
|
||||
|
||||
func (sp *slicer) next(n int) (b []byte) {
|
||||
s := *sp
|
||||
b, *sp = s[0:n], s[n:]
|
||||
return
|
||||
}
|
||||
|
||||
func isASCII(s string) bool {
|
||||
for _, c := range s {
|
||||
if c >= 0x80 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func toASCII(s string) string {
|
||||
if isASCII(s) {
|
||||
return s
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, c := range s {
|
||||
if c < 0x80 {
|
||||
buf.WriteByte(byte(c))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// isHeaderOnlyType checks if the given type flag is of the type that has no
|
||||
// data section even if a size is specified.
|
||||
func isHeaderOnlyType(flag byte) bool {
|
||||
switch flag {
|
||||
case TypeLink, TypeSymlink, TypeChar, TypeBlock, TypeDir, TypeFifo:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tar_test
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
// Create a buffer to write our archive to.
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// Create a new tar archive.
|
||||
tw := tar.NewWriter(buf)
|
||||
|
||||
// Add some files to the archive.
|
||||
var files = []struct {
|
||||
Name, Body string
|
||||
}{
|
||||
{"readme.txt", "This archive contains some text files."},
|
||||
{"gopher.txt", "Gopher names:\nGeorge\nGeoffrey\nGonzo"},
|
||||
{"todo.txt", "Get animal handling license."},
|
||||
}
|
||||
for _, file := range files {
|
||||
hdr := &tar.Header{
|
||||
Name: file.Name,
|
||||
Mode: 0600,
|
||||
Size: int64(len(file.Body)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if _, err := tw.Write([]byte(file.Body)); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
// Make sure to check the error on Close.
|
||||
if err := tw.Close(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Open the tar archive for reading.
|
||||
r := bytes.NewReader(buf.Bytes())
|
||||
tr := tar.NewReader(r)
|
||||
|
||||
// Iterate through the files in the archive.
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
// end of tar archive
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Printf("Contents of %s:\n", hdr.Name)
|
||||
if _, err := io.Copy(os.Stdout, tr); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Contents of readme.txt:
|
||||
// This archive contains some text files.
|
||||
// Contents of gopher.txt:
|
||||
// Gopher names:
|
||||
// George
|
||||
// Geoffrey
|
||||
// Gonzo
|
||||
// Contents of todo.txt:
|
||||
// Get animal handling license.
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux dragonfly openbsd solaris
|
||||
|
||||
package tar
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func statAtime(st *syscall.Stat_t) time.Time {
|
||||
return time.Unix(st.Atim.Unix())
|
||||
}
|
||||
|
||||
func statCtime(st *syscall.Stat_t) time.Time {
|
||||
return time.Unix(st.Ctim.Unix())
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin freebsd netbsd
|
||||
|
||||
package tar
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func statAtime(st *syscall.Stat_t) time.Time {
|
||||
return time.Unix(st.Atimespec.Unix())
|
||||
}
|
||||
|
||||
func statCtime(st *syscall.Stat_t) time.Time {
|
||||
return time.Unix(st.Ctimespec.Unix())
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux darwin dragonfly freebsd openbsd netbsd solaris
|
||||
|
||||
package tar
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sysStat = statUnix
|
||||
}
|
||||
|
||||
func statUnix(fi os.FileInfo, h *Header) error {
|
||||
sys, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
h.Uid = int(sys.Uid)
|
||||
h.Gid = int(sys.Gid)
|
||||
// TODO(bradfitz): populate username & group. os/user
|
||||
// doesn't cache LookupId lookups, and lacks group
|
||||
// lookup functions.
|
||||
h.AccessTime = statAtime(sys)
|
||||
h.ChangeTime = statCtime(sys)
|
||||
// TODO(bradfitz): major/minor device numbers?
|
||||
return nil
|
||||
}
|
@ -0,0 +1,325 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFileInfoHeader(t *testing.T) {
|
||||
fi, err := os.Stat("testdata/small.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := FileInfoHeader(fi, "")
|
||||
if err != nil {
|
||||
t.Fatalf("FileInfoHeader: %v", err)
|
||||
}
|
||||
if g, e := h.Name, "small.txt"; g != e {
|
||||
t.Errorf("Name = %q; want %q", g, e)
|
||||
}
|
||||
if g, e := h.Mode, int64(fi.Mode().Perm())|c_ISREG; g != e {
|
||||
t.Errorf("Mode = %#o; want %#o", g, e)
|
||||
}
|
||||
if g, e := h.Size, int64(5); g != e {
|
||||
t.Errorf("Size = %v; want %v", g, e)
|
||||
}
|
||||
if g, e := h.ModTime, fi.ModTime(); !g.Equal(e) {
|
||||
t.Errorf("ModTime = %v; want %v", g, e)
|
||||
}
|
||||
// FileInfoHeader should error when passing nil FileInfo
|
||||
if _, err := FileInfoHeader(nil, ""); err == nil {
|
||||
t.Fatalf("Expected error when passing nil to FileInfoHeader")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoHeaderDir(t *testing.T) {
|
||||
fi, err := os.Stat("testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := FileInfoHeader(fi, "")
|
||||
if err != nil {
|
||||
t.Fatalf("FileInfoHeader: %v", err)
|
||||
}
|
||||
if g, e := h.Name, "testdata/"; g != e {
|
||||
t.Errorf("Name = %q; want %q", g, e)
|
||||
}
|
||||
// Ignoring c_ISGID for golang.org/issue/4867
|
||||
if g, e := h.Mode&^c_ISGID, int64(fi.Mode().Perm())|c_ISDIR; g != e {
|
||||
t.Errorf("Mode = %#o; want %#o", g, e)
|
||||
}
|
||||
if g, e := h.Size, int64(0); g != e {
|
||||
t.Errorf("Size = %v; want %v", g, e)
|
||||
}
|
||||
if g, e := h.ModTime, fi.ModTime(); !g.Equal(e) {
|
||||
t.Errorf("ModTime = %v; want %v", g, e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoHeaderSymlink(t *testing.T) {
|
||||
h, err := FileInfoHeader(symlink{}, "some-target")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, e := h.Name, "some-symlink"; g != e {
|
||||
t.Errorf("Name = %q; want %q", g, e)
|
||||
}
|
||||
if g, e := h.Linkname, "some-target"; g != e {
|
||||
t.Errorf("Linkname = %q; want %q", g, e)
|
||||
}
|
||||
}
|
||||
|
||||
type symlink struct{}
|
||||
|
||||
func (symlink) Name() string { return "some-symlink" }
|
||||
func (symlink) Size() int64 { return 0 }
|
||||
func (symlink) Mode() os.FileMode { return os.ModeSymlink }
|
||||
func (symlink) ModTime() time.Time { return time.Time{} }
|
||||
func (symlink) IsDir() bool { return false }
|
||||
func (symlink) Sys() interface{} { return nil }
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
data := []byte("some file contents")
|
||||
|
||||
var b bytes.Buffer
|
||||
tw := NewWriter(&b)
|
||||
hdr := &Header{
|
||||
Name: "file.txt",
|
||||
Uid: 1 << 21, // too big for 8 octal digits
|
||||
Size: int64(len(data)),
|
||||
ModTime: time.Now(),
|
||||
}
|
||||
// tar only supports second precision.
|
||||
hdr.ModTime = hdr.ModTime.Add(-time.Duration(hdr.ModTime.Nanosecond()) * time.Nanosecond)
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
t.Fatalf("tw.WriteHeader: %v", err)
|
||||
}
|
||||
if _, err := tw.Write(data); err != nil {
|
||||
t.Fatalf("tw.Write: %v", err)
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
t.Fatalf("tw.Close: %v", err)
|
||||
}
|
||||
|
||||
// Read it back.
|
||||
tr := NewReader(&b)
|
||||
rHdr, err := tr.Next()
|
||||
if err != nil {
|
||||
t.Fatalf("tr.Next: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(rHdr, hdr) {
|
||||
t.Errorf("Header mismatch.\n got %+v\nwant %+v", rHdr, hdr)
|
||||
}
|
||||
rData, err := ioutil.ReadAll(tr)
|
||||
if err != nil {
|
||||
t.Fatalf("Read: %v", err)
|
||||
}
|
||||
if !bytes.Equal(rData, data) {
|
||||
t.Errorf("Data mismatch.\n got %q\nwant %q", rData, data)
|
||||
}
|
||||
}
|
||||
|
||||
type headerRoundTripTest struct {
|
||||
h *Header
|
||||
fm os.FileMode
|
||||
}
|
||||
|
||||
func TestHeaderRoundTrip(t *testing.T) {
|
||||
golden := []headerRoundTripTest{
|
||||
// regular file.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "test.txt",
|
||||
Mode: 0644 | c_ISREG,
|
||||
Size: 12,
|
||||
ModTime: time.Unix(1360600916, 0),
|
||||
Typeflag: TypeReg,
|
||||
},
|
||||
fm: 0644,
|
||||
},
|
||||
// symbolic link.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "link.txt",
|
||||
Mode: 0777 | c_ISLNK,
|
||||
Size: 0,
|
||||
ModTime: time.Unix(1360600852, 0),
|
||||
Typeflag: TypeSymlink,
|
||||
},
|
||||
fm: 0777 | os.ModeSymlink,
|
||||
},
|
||||
// character device node.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "dev/null",
|
||||
Mode: 0666 | c_ISCHR,
|
||||
Size: 0,
|
||||
ModTime: time.Unix(1360578951, 0),
|
||||
Typeflag: TypeChar,
|
||||
},
|
||||
fm: 0666 | os.ModeDevice | os.ModeCharDevice,
|
||||
},
|
||||
// block device node.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "dev/sda",
|
||||
Mode: 0660 | c_ISBLK,
|
||||
Size: 0,
|
||||
ModTime: time.Unix(1360578954, 0),
|
||||
Typeflag: TypeBlock,
|
||||
},
|
||||
fm: 0660 | os.ModeDevice,
|
||||
},
|
||||
// directory.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "dir/",
|
||||
Mode: 0755 | c_ISDIR,
|
||||
Size: 0,
|
||||
ModTime: time.Unix(1360601116, 0),
|
||||
Typeflag: TypeDir,
|
||||
},
|
||||
fm: 0755 | os.ModeDir,
|
||||
},
|
||||
// fifo node.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "dev/initctl",
|
||||
Mode: 0600 | c_ISFIFO,
|
||||
Size: 0,
|
||||
ModTime: time.Unix(1360578949, 0),
|
||||
Typeflag: TypeFifo,
|
||||
},
|
||||
fm: 0600 | os.ModeNamedPipe,
|
||||
},
|
||||
// setuid.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "bin/su",
|
||||
Mode: 0755 | c_ISREG | c_ISUID,
|
||||
Size: 23232,
|
||||
ModTime: time.Unix(1355405093, 0),
|
||||
Typeflag: TypeReg,
|
||||
},
|
||||
fm: 0755 | os.ModeSetuid,
|
||||
},
|
||||
// setguid.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "group.txt",
|
||||
Mode: 0750 | c_ISREG | c_ISGID,
|
||||
Size: 0,
|
||||
ModTime: time.Unix(1360602346, 0),
|
||||
Typeflag: TypeReg,
|
||||
},
|
||||
fm: 0750 | os.ModeSetgid,
|
||||
},
|
||||
// sticky.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "sticky.txt",
|
||||
Mode: 0600 | c_ISREG | c_ISVTX,
|
||||
Size: 7,
|
||||
ModTime: time.Unix(1360602540, 0),
|
||||
Typeflag: TypeReg,
|
||||
},
|
||||
fm: 0600 | os.ModeSticky,
|
||||
},
|
||||
// hard link.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "hard.txt",
|
||||
Mode: 0644 | c_ISREG,
|
||||
Size: 0,
|
||||
Linkname: "file.txt",
|
||||
ModTime: time.Unix(1360600916, 0),
|
||||
Typeflag: TypeLink,
|
||||
},
|
||||
fm: 0644,
|
||||
},
|
||||
// More information.
|
||||
{
|
||||
h: &Header{
|
||||
Name: "info.txt",
|
||||
Mode: 0600 | c_ISREG,
|
||||
Size: 0,
|
||||
Uid: 1000,
|
||||
Gid: 1000,
|
||||
ModTime: time.Unix(1360602540, 0),
|
||||
Uname: "slartibartfast",
|
||||
Gname: "users",
|
||||
Typeflag: TypeReg,
|
||||
},
|
||||
fm: 0600,
|
||||
},
|
||||
}
|
||||
|
||||
for i, g := range golden {
|
||||
fi := g.h.FileInfo()
|
||||
h2, err := FileInfoHeader(fi, "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(fi.Name(), "/") {
|
||||
t.Errorf("FileInfo of %q contains slash: %q", g.h.Name, fi.Name())
|
||||
}
|
||||
name := path.Base(g.h.Name)
|
||||
if fi.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
if got, want := h2.Name, name; got != want {
|
||||
t.Errorf("i=%d: Name: got %v, want %v", i, got, want)
|
||||
}
|
||||
if got, want := h2.Size, g.h.Size; got != want {
|
||||
t.Errorf("i=%d: Size: got %v, want %v", i, got, want)
|
||||
}
|
||||
if got, want := h2.Uid, g.h.Uid; got != want {
|
||||
t.Errorf("i=%d: Uid: got %d, want %d", i, got, want)
|
||||
}
|
||||
if got, want := h2.Gid, g.h.Gid; got != want {
|
||||
t.Errorf("i=%d: Gid: got %d, want %d", i, got, want)
|
||||
}
|
||||
if got, want := h2.Uname, g.h.Uname; got != want {
|
||||
t.Errorf("i=%d: Uname: got %q, want %q", i, got, want)
|
||||
}
|
||||
if got, want := h2.Gname, g.h.Gname; got != want {
|
||||
t.Errorf("i=%d: Gname: got %q, want %q", i, got, want)
|
||||
}
|
||||
if got, want := h2.Linkname, g.h.Linkname; got != want {
|
||||
t.Errorf("i=%d: Linkname: got %v, want %v", i, got, want)
|
||||
}
|
||||
if got, want := h2.Typeflag, g.h.Typeflag; got != want {
|
||||
t.Logf("%#v %#v", g.h, fi.Sys())
|
||||
t.Errorf("i=%d: Typeflag: got %q, want %q", i, got, want)
|
||||
}
|
||||
if got, want := h2.Mode, g.h.Mode; got != want {
|
||||
t.Errorf("i=%d: Mode: got %o, want %o", i, got, want)
|
||||
}
|
||||
if got, want := fi.Mode(), g.fm; got != want {
|
||||
t.Errorf("i=%d: fi.Mode: got %o, want %o", i, got, want)
|
||||
}
|
||||
if got, want := h2.AccessTime, g.h.AccessTime; got != want {
|
||||
t.Errorf("i=%d: AccessTime: got %v, want %v", i, got, want)
|
||||
}
|
||||
if got, want := h2.ChangeTime, g.h.ChangeTime; got != want {
|
||||
t.Errorf("i=%d: ChangeTime: got %v, want %v", i, got, want)
|
||||
}
|
||||
if got, want := h2.ModTime, g.h.ModTime; got != want {
|
||||
t.Errorf("i=%d: ModTime: got %v, want %v", i, got, want)
|
||||
}
|
||||
if sysh, ok := fi.Sys().(*Header); !ok || sysh != g.h {
|
||||
t.Errorf("i=%d: Sys didn't return original *Header", i)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
Kilts
|
@ -0,0 +1 @@
|
||||
Google.com
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue