pull/332/merge
JG² 7 years ago committed by GitHub
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,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)
}
}

210
glide.lock generated

@ -1,6 +1,12 @@
hash: 90e72dd70a9102940fc6126afc2d7d73ee79faaf75cecb3710b15f5a121c67ce
updated: 2016-11-11T15:36:41.356471726+01:00
hash: 66c9fb1044a56834964d8f96605217630d1178d15559617ed59a3446c49ab8b1
updated: 2017-02-27T15:55:20.907327822+01:00
imports:
- name: github.com/artyom/untar
version: 02ed5a2449a661eb02b1e3d658524223ab726412
- name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages:
- winterm
- name: github.com/beorn7/perks
version: b965b613227fddccbfffe13eae360ed3fa822f8d
subpackages:
@ -20,16 +26,113 @@ imports:
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- name: github.com/docker/distribution
version: 129ad8ea0c3760d878b34cffdb9c3be874a7b2f7
subpackages:
- context
- digestset
- manifest
- manifest/manifestlist
- manifest/schema1
- manifest/schema2
- reference
- registry/api/errcode
- registry/api/v2
- registry/client
- registry/client/auth
- registry/client/auth/challenge
- registry/client/transport
- registry/storage/cache
- registry/storage/cache/memory
- uuid
- name: github.com/docker/docker
version: 48dd90d3985889ca008faa3b041bf31d2ada95c5
subpackages:
- api
- api/types
- api/types/blkiodev
- api/types/container
- api/types/events
- api/types/filters
- api/types/image
- api/types/mount
- api/types/network
- api/types/reference
- api/types/registry
- api/types/strslice
- api/types/swarm
- api/types/time
- api/types/versions
- api/types/volume
- cli/config
- cli/config/configfile
- client
- daemon/graphdriver
- distribution
- distribution/metadata
- distribution/xfer
- dockerversion
- image
- image/v1
- layer
- oci
- opts
- pkg/archive
- pkg/chrootarchive
- pkg/fileutils
- pkg/homedir
- pkg/httputils
- pkg/idtools
- pkg/ioutils
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/mount
- pkg/parsers/kernel
- pkg/plugingetter
- pkg/plugins
- pkg/plugins/transport
- pkg/pools
- pkg/progress
- pkg/promise
- pkg/random
- pkg/reexec
- pkg/stringid
- pkg/system
- pkg/tarsum
- pkg/term
- pkg/term/windows
- pkg/tlsconfig
- pkg/useragent
- plugin/v2
- reference
- registry
- name: github.com/docker/go-connections
version: 4ccf312bf1d35e5dbda654e57a9be4c3f3cd0366
subpackages:
- nat
- sockets
- tlsconfig
- name: github.com/docker/go-units
version: f2d77a61e3c169b43402a0a1e84f06daf29b8190
- name: github.com/docker/libtrust
version: fa567046d9b14f6aa788882a950d69651d230b21
- name: github.com/fatih/color
version: 1b35f289c47d5c73c398cea8e006b7bcb6234a96
version: 87d4004f2ab62d0d255e0a38f1680aa534549fe3
- name: github.com/fernet/fernet-go
version: 1b2437bc582b3cfbb341ee5a29f8ef5b42912ff2
- name: github.com/fsnotify/fsnotify
version: bd2828f9f176e52d7222e565abb2d338d3f3c103
- name: github.com/go-sql-driver/mysql
version: d512f204a577a4ab037a1816604c48c9c13210be
- name: github.com/golang/protobuf
version: 5fc2294e655b78ed8a02082d37808d46c17d7e64
subpackages:
- proto
- name: github.com/gorilla/context
version: 14f550f51af52180c2eefed15e5fd18d63c0a64a
- name: github.com/gorilla/mux
version: e444e69cbd2e2e3e0749a2f3c717cec491552bbf
- name: github.com/guregu/null
version: 79c5bd36b615db4c06132321189f579c8a5fca98
subpackages:
@ -38,8 +141,25 @@ imports:
version: 5c7531c003d8bf158b0fe5063649a2f41a822146
subpackages:
- simplelru
- name: github.com/hashicorp/hcl
version: 99ce73d4fe576449f7a689d4fc2b2ad09a86bdaa
subpackages:
- hcl/ast
- hcl/parser
- hcl/scanner
- hcl/strconv
- hcl/token
- json/parser
- json/scanner
- json/token
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/jgsqware/xnet
version: 13630f0737d214dba8344df649039c19551553d8
- name: github.com/julienschmidt/httprouter
version: 21439ef4d70ba4f3e2a5ed9249e7b03af4019b40
- name: github.com/kr/fs
version: 2788f0dbd16903de03cb8186e5c7d97b69ad387b
- name: github.com/kr/text
version: 7cafcd837844e784b526369c9bce262804aebc60
- name: github.com/kylelemons/go-gypsy
@ -50,14 +170,45 @@ imports:
version: 11fc39a580a008f1f39bb3d11d984fb34ed778d9
subpackages:
- oid
- name: github.com/magiconair/properties
version: 0723e352fa358f9322c938cc2dadda874e9151a9
- name: github.com/mattn/go-shellwords
version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24
- name: github.com/mattn/go-sqlite3
version: 5510da399572b4962c020184bb291120c0a412e2
- name: github.com/matttproud/golang_protobuf_extensions
version: d0c3fe89de86839aecf2e0579c40ba3bb336a453
subpackages:
- pbutil
- name: github.com/Microsoft/go-winio
version: ce2922f643c8fd76b46cadc7f404a06282678b34
- name: github.com/mitchellh/mapstructure
version: 482a9fd5fa83e8c4e7817413b80f3eb8feec03ef
- name: github.com/Nvveen/Gotty
version: cd527374f1e5bff4938207604a14f2e38a9cf512
- name: github.com/opencontainers/go-digest
version: 21dfd564fd89c944783d00d069f33e3e7123c448
- name: github.com/opencontainers/runc
version: 157a96a42860b16b50401d131e257c7d7f281d7b
subpackages:
- libcontainer/configs
- libcontainer/devices
- libcontainer/system
- libcontainer/user
- name: github.com/opencontainers/runtime-spec
version: 1c7c27d043c2a5e513a44084d2b10d77d1402b8c
subpackages:
- specs-go
- name: github.com/pborman/uuid
version: dee7705ef7b324f27ceb85a121c61f2c2e8ce988
- name: github.com/pelletier/go-buffruneio
version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d
- name: github.com/pelletier/go-toml
version: 45932ad32dfdd20826f5671da37a5f3ce9f26a8d
- name: github.com/pkg/errors
version: 839d9e913e063e28dfd0e6c7b7512793e0a48be9
- name: github.com/pkg/sftp
version: 4d0e916071f68db74f8a73926335f809396d6b42
- name: github.com/pmezard/go-difflib
version: e8554b8641db39598be7f6342874b958f12ae1d4
subpackages:
@ -80,24 +231,69 @@ imports:
version: 406e5b7bfd8201a36e2bb5f7bdae0b03380c2ce8
- name: github.com/remind101/migrate
version: d22d647232c20dbea6d2aa1dda7f2737cccce614
- name: github.com/shiena/ansicolor
version: a422bbe96644373c5753384a59d678f7d261ff10
- name: github.com/Sirupsen/logrus
version: d26492970760ca5d33129d2d799e34be5c4782eb
- name: github.com/spf13/afero
version: 52e4a6cfac46163658bd4f123c49b6ee7dc75f78
subpackages:
- mem
- sftp
- name: github.com/spf13/cast
version: 2580bc98dc0e62908119e4737030cc2fdfc45e4c
- name: github.com/spf13/cobra
version: 6e91dded25d73176bf7f60b40dd7aa1f0bf9be8d
- name: github.com/spf13/jwalterweatherman
version: 33c24e77fb80341fe7130ee7c594256ff08ccc46
- name: github.com/spf13/pflag
version: 7597b2702729ebb651fc9bb2adac40bcc62db82d
- name: github.com/spf13/viper
version: 80ab6657f9ec7e5761f6603320d3d58dfe6970f6
- name: github.com/stretchr/testify
version: 5b9da39b66e8e994455c2525c4421c8cc00a7f93
subpackages:
- assert
- name: github.com/tylerb/graceful
version: 84177357ab104029f9237abcb52339a7b80760ef
version: 4654dfbb6ad53cb5e27f37d99b02e16c1872fbbb
- name: github.com/vbatts/tar-split
version: bd4c5d64c3e9297f410025a3b1bd0c58f659e721
subpackages:
- archive/tar
- tar/asm
- tar/storage
- name: github.com/ziutek/mymysql
version: 75ce5fbba34b1912a3641adbd58cf317d7315821
subpackages:
- godrv
- mysql
- native
- name: golang.org/x/crypto
version: ca7e7f10cb9fd9c1a6ff7f60436c086d73714180
subpackages:
- curve25519
- ed25519
- ed25519/internal/edwards25519
- ssh
- name: golang.org/x/net
version: 1d7a0b2100da090d8b02afcfb42f97e2c77e71a4
subpackages:
- context
- context/ctxhttp
- netutil
- proxy
- name: golang.org/x/sys
version: c200b10b5d5e122be351b67af224adc6128af5bf
subpackages:
- unix
- windows
- name: golang.org/x/text
version: 16e1d1f27f7aba51c74c0aeb7a7ee31a75c5c63c
subpackages:
- transform
- unicode/norm
- name: golang.org/x/time
version: a4bde12657593d5e90d0533a3e4fd95e635124cb
subpackages:
- rate
- name: gopkg.in/yaml.v2
version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4
devImports: []
testImports: []

@ -91,3 +91,34 @@ import:
version: ^0.1.0
- package: github.com/kr/text
- package: github.com/remind101/migrate
- package: github.com/artyom/untar
- package: github.com/docker/distribution
version: 129ad8ea0c3760d878b34cffdb9c3be874a7b2f7
subpackages:
- manifest/schema1
- registry/client
- package: github.com/docker/go-connections
version: 4ccf312bf1d35e5dbda654e57a9be4c3f3cd0366
subpackages:
- tlsconfig
- package: github.com/spf13/cobra
- package: github.com/spf13/viper
- package: github.com/docker/go-units
version: ^0.3.1
- package: github.com/docker/docker
version: 48dd90d3985889ca008faa3b041bf31d2ada95c5
subpackages:
- api/types
- cli/config
- distribution
- reference
- registry
- package: github.com/spf13/pflag
version: 7597b2702729ebb651fc9bb2adac40bcc62db82d
- package: golang.org/x/crypto
version: ca7e7f10cb9fd9c1a6ff7f60436c086d73714180
- package: github.com/opencontainers/go-digest
version: 21dfd564fd89c944783d00d069f33e3e7123c448
- package: github.com/jgsqware/xnet
- package: github.com/Sirupsen/logrus
version: v0.11.0

@ -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}, &region)
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, &region); 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
}

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) [2014] [shiena]
Copyright (c) 2015 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -19,3 +19,4 @@ 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,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)
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save