pylego 0.1.32__py3-none-any.whl → 0.1.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pylego/lego.go CHANGED
@@ -11,9 +11,12 @@ import (
11
11
  "encoding/pem"
12
12
  "errors"
13
13
  "fmt"
14
+ "net"
14
15
  "os"
16
+ "strings"
15
17
  "time"
16
18
 
19
+ "github.com/go-acme/lego/v4/acme"
17
20
  "github.com/go-acme/lego/v4/certcrypto"
18
21
  "github.com/go-acme/lego/v4/certificate"
19
22
  "github.com/go-acme/lego/v4/challenge/dns01"
@@ -24,6 +27,22 @@ import (
24
27
  "github.com/go-acme/lego/v4/registration"
25
28
  )
26
29
 
30
+ // Error code constants
31
+ const (
32
+ ErrInvalidArguments = "invalid_arguments"
33
+ ErrInvalidEnvironment = "invalid_environment"
34
+ ErrCertificateRequestFailed = "certificate_request_failed"
35
+ ErrInvalidPrivateKey = "invalid_private_key"
36
+ ErrKeyGenerationFailed = "key_generation_failed"
37
+ ErrLegoClientCreationFailed = "lego_client_creation_failed"
38
+ ErrDNSProviderFailed = "dns_provider_failed"
39
+ ErrAccountRegistrationFailed = "account_registration_failed"
40
+ ErrInvalidCSR = "invalid_csr"
41
+ ErrCertificateObtainFailed = "certificate_obtain_failed"
42
+ ErrNetworkError = "network_error"
43
+ ErrMarshalingFailed = "marshaling_failed"
44
+ )
45
+
27
46
  type LegoInputArgs struct {
28
47
  Email string `json:"email"`
29
48
  PrivateKey string `json:"private_key,omitempty"`
@@ -48,28 +67,203 @@ type Metadata struct {
48
67
  Domain string `json:"domain"`
49
68
  }
50
69
 
70
+ type Subproblem struct {
71
+ Type string `json:"type"` // Error type URN
72
+ Detail string `json:"detail"` // Human-readable message
73
+ Identifier Identifier `json:"identifier,omitempty"` // The identifier that caused this subproblem
74
+ }
75
+
76
+ type Identifier struct {
77
+ Type string `json:"type"` // "dns" or "ip"
78
+ Value string `json:"value"` // Domain name or IP address
79
+ }
80
+
81
+ type ErrorResponse struct {
82
+ Type string `json:"type"` // "acme" for CA server errors, "lego" for everything else
83
+ Code string `json:"code"` // Error code or category
84
+ Status *int `json:"status,omitempty"` // HTTP status if applicable (ACME errors)
85
+ Detail string `json:"detail"` // Human-readable message
86
+ ACMEType string `json:"acme_type,omitempty"` // Full ACME URN if applicable
87
+ Subproblems []Subproblem `json:"subproblems,omitempty"` // Detailed subproblems from ACME errors
88
+ }
89
+
90
+ type LegoResponse struct {
91
+ Success bool `json:"success"`
92
+ Error *ErrorResponse `json:"error,omitempty"`
93
+ Data *LegoOutputResponse `json:"data,omitempty"`
94
+ }
95
+
96
+ func isNetworkError(err error) bool {
97
+ if err == nil {
98
+ return false
99
+ }
100
+ var (
101
+ netErr net.Error
102
+ dnsErr *net.DNSError
103
+ opErr *net.OpError
104
+ )
105
+
106
+ switch {
107
+ case errors.As(err, &netErr):
108
+ return true
109
+ case errors.As(err, &dnsErr):
110
+ return true
111
+ case errors.As(err, &opErr):
112
+ return true
113
+ default:
114
+ return false
115
+ }
116
+ }
117
+
118
+ func extractSubproblems(problemDetails *acme.ProblemDetails) []Subproblem {
119
+ var subproblems []Subproblem
120
+ for _, sub := range problemDetails.SubProblems {
121
+ subCode := "unknown"
122
+ if sub.Type != "" {
123
+ parts := strings.Split(sub.Type, ":")
124
+ if len(parts) > 0 {
125
+ subCode = parts[len(parts)-1]
126
+ }
127
+ }
128
+ subproblems = append(subproblems, Subproblem{
129
+ Type: subCode,
130
+ Detail: sub.Detail,
131
+ Identifier: Identifier{
132
+ Type: sub.Identifier.Type,
133
+ Value: sub.Identifier.Value,
134
+ },
135
+ })
136
+ }
137
+ return subproblems
138
+ }
139
+
140
+ func wrapError(err error, context string) *ErrorResponse {
141
+ if err == nil {
142
+ return nil
143
+ }
144
+
145
+ var ctxErr *contextError
146
+ if errors.As(err, &ctxErr) {
147
+ context = ctxErr.context
148
+ err = ctxErr.original
149
+ }
150
+
151
+ var problemDetails *acme.ProblemDetails
152
+ if errors.As(err, &problemDetails) {
153
+ code := "unknown"
154
+ if problemDetails.Type != "" {
155
+ parts := strings.Split(problemDetails.Type, ":")
156
+ if len(parts) > 0 {
157
+ code = parts[len(parts)-1]
158
+ }
159
+ }
160
+ status := problemDetails.HTTPStatus
161
+
162
+ return &ErrorResponse{
163
+ Type: "acme",
164
+ Code: code,
165
+ Status: &status,
166
+ Detail: problemDetails.Detail,
167
+ ACMEType: problemDetails.Type,
168
+ Subproblems: extractSubproblems(problemDetails),
169
+ }
170
+ }
171
+
172
+ if isNetworkError(err) {
173
+ return &ErrorResponse{
174
+ Type: "lego",
175
+ Code: ErrNetworkError,
176
+ Detail: err.Error(),
177
+ }
178
+ }
179
+
180
+ return &ErrorResponse{
181
+ Type: "lego",
182
+ Code: context,
183
+ Detail: err.Error(),
184
+ }
185
+ }
186
+
187
+ func buildErrorResponse(err error, context string) *C.char {
188
+ response := LegoResponse{
189
+ Success: false,
190
+ Error: wrapError(err, context),
191
+ }
192
+ responseJSON, marshalErr := json.Marshal(response)
193
+ if marshalErr == nil {
194
+ return C.CString(string(responseJSON))
195
+ }
196
+
197
+ fallbackResponse := LegoResponse{
198
+ Success: false,
199
+ Error: &ErrorResponse{
200
+ Type: "lego",
201
+ Code: ErrMarshalingFailed,
202
+ Detail: marshalErr.Error(),
203
+ },
204
+ }
205
+ fallbackJSON, _ := json.Marshal(fallbackResponse)
206
+
207
+ return C.CString(string(fallbackJSON))
208
+ }
209
+
210
+ func buildSuccessResponse(data *LegoOutputResponse) *C.char {
211
+ response := LegoResponse{
212
+ Success: true,
213
+ Data: data,
214
+ }
215
+ responseJSON, marshalErr := json.Marshal(response)
216
+ if marshalErr == nil {
217
+ return C.CString(string(responseJSON))
218
+ }
219
+
220
+ fallbackResponse := LegoResponse{
221
+ Success: false,
222
+ Error: &ErrorResponse{
223
+ Type: "lego",
224
+ Code: ErrMarshalingFailed,
225
+ Detail: marshalErr.Error(),
226
+ },
227
+ }
228
+ fallbackJSON, _ := json.Marshal(fallbackResponse)
229
+
230
+ return C.CString(string(fallbackJSON))
231
+ }
232
+
233
+ type contextError struct {
234
+ original error
235
+ context string
236
+ }
237
+
238
+ func (e *contextError) Error() string {
239
+ return e.original.Error()
240
+ }
241
+
242
+ func (e *contextError) Unwrap() error {
243
+ return e.original
244
+ }
245
+
246
+ func wrapWithContext(err error, context string) error {
247
+ return &contextError{original: err, context: context}
248
+ }
249
+
51
250
  //export RunLegoCommand
52
251
  func RunLegoCommand(message *C.char) *C.char {
53
252
  CLIArgs, err := extractArguments(C.GoString(message))
54
253
  if err != nil {
55
- return C.CString(fmt.Sprint("error: couldn't extract arguments: ", err))
254
+ return buildErrorResponse(err, ErrInvalidArguments)
56
255
  }
57
256
  for k, v := range CLIArgs.Env {
58
257
  if err := os.Setenv(k, v); err != nil {
59
- return C.CString(fmt.Sprint("error: couldn't load environment variables: ", err))
258
+ return buildErrorResponse(err, ErrInvalidEnvironment)
60
259
  }
61
260
 
62
261
  }
63
262
  certificate, err := requestCertificate(CLIArgs.Email, CLIArgs.PrivateKey, CLIArgs.Server, CLIArgs.CSR, CLIArgs.Plugin, CLIArgs.DNSPropagationWait)
64
263
  if err != nil {
65
- return C.CString(fmt.Sprint("error: couldn't request certificate: ", err))
264
+ return buildErrorResponse(err, ErrCertificateRequestFailed)
66
265
  }
67
- response_json, err := json.Marshal(certificate)
68
- if err != nil {
69
- return C.CString(fmt.Sprint("error: coudn't build response message: ", err))
70
- }
71
- return_message_ptr := C.CString(string(response_json))
72
- return return_message_ptr
266
+ return buildSuccessResponse(certificate)
73
267
  }
74
268
 
75
269
  func requestCertificate(email, privateKeyPem, server, csr, plugin string, propagationWait int) (*LegoOutputResponse, error) {
@@ -77,13 +271,13 @@ func requestCertificate(email, privateKeyPem, server, csr, plugin string, propag
77
271
  if privateKeyPem != "" {
78
272
  parsedKey, err := certcrypto.ParsePEMPrivateKey([]byte(privateKeyPem))
79
273
  if err != nil {
80
- return nil, fmt.Errorf("couldn't parse private key: %s", err)
274
+ return nil, wrapWithContext(err, ErrInvalidPrivateKey)
81
275
  }
82
276
  privateKey = parsedKey
83
277
  } else {
84
278
  generatedKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
85
279
  if err != nil {
86
- return nil, fmt.Errorf("couldn't generate priv key: %s", err)
280
+ return nil, wrapWithContext(err, ErrKeyGenerationFailed)
87
281
  }
88
282
  privateKey = generatedKey
89
283
  }
@@ -98,27 +292,27 @@ func requestCertificate(email, privateKeyPem, server, csr, plugin string, propag
98
292
 
99
293
  client, err := lego.NewClient(config)
100
294
  if err != nil {
101
- return nil, fmt.Errorf("couldn't create lego client: %s", err)
295
+ return nil, wrapWithContext(err, ErrLegoClientCreationFailed)
102
296
  }
103
297
 
104
298
  err = configureClientChallenges(client, plugin, propagationWait)
105
299
  if err != nil {
106
- return nil, fmt.Errorf("couldn't configure client challenges: %s", err)
300
+ return nil, wrapWithContext(err, ErrDNSProviderFailed)
107
301
  }
108
302
 
109
303
  reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
110
304
  if err != nil {
111
- return nil, fmt.Errorf("couldn't register user: %s", err)
305
+ return nil, wrapWithContext(err, ErrAccountRegistrationFailed)
112
306
  }
113
307
  user.Registration = reg
114
308
 
115
309
  block, _ := pem.Decode([]byte(csr))
116
310
  if block == nil || block.Type != "CERTIFICATE REQUEST" {
117
- return nil, errors.New("failed to decode PEM block containing certificate request")
311
+ return nil, wrapWithContext(errors.New("failed to decode PEM block"), ErrInvalidCSR)
118
312
  }
119
313
  csrObject, err := x509.ParseCertificateRequest(block.Bytes)
120
314
  if err != nil {
121
- return nil, fmt.Errorf("failed to parse certificate request: %s", err)
315
+ return nil, wrapWithContext(err, ErrInvalidCSR)
122
316
  }
123
317
  request := certificate.ObtainForCSRRequest{
124
318
  CSR: csrObject,
@@ -126,7 +320,7 @@ func requestCertificate(email, privateKeyPem, server, csr, plugin string, propag
126
320
  }
127
321
  certificates, err := client.Certificate.ObtainForCSR(request)
128
322
  if err != nil {
129
- return nil, fmt.Errorf("coudn't obtain cert: %s", err)
323
+ return nil, wrapWithContext(err, ErrCertificateObtainFailed)
130
324
  }
131
325
 
132
326
  return &LegoOutputResponse{
@@ -160,9 +354,6 @@ func configureClientChallenges(client *lego.Client, plugin string, propagationWa
160
354
  return errors.Join(fmt.Errorf("couldn't create %s provider: ", plugin), err)
161
355
  }
162
356
  var wait time.Duration
163
- if propagationWait < 0 {
164
- return fmt.Errorf("DNS_PROPAGATION_WAIT cannot be negative: %d", propagationWait)
165
- }
166
357
  if propagationWait > 0 {
167
358
  wait = time.Duration(propagationWait) * time.Second
168
359
  }
pylego/lego.so CHANGED
Binary file
pylego/pylego.py CHANGED
@@ -10,6 +10,23 @@ so_file = here / ("lego.so")
10
10
  library = ctypes.cdll.LoadLibrary(so_file)
11
11
 
12
12
 
13
+ @dataclass
14
+ class Identifier:
15
+ """ACME identifier (domain or IP)."""
16
+
17
+ type: str # "dns" or "ip"
18
+ value: str # Domain name or IP address
19
+
20
+
21
+ @dataclass
22
+ class Subproblem:
23
+ """ACME subproblem details."""
24
+
25
+ type: str # Error type (e.g., "unauthorized", "dns")
26
+ detail: str # Human-readable message
27
+ identifier: Identifier # The identifier that caused this subproblem
28
+
29
+
13
30
  @dataclass
14
31
  class Metadata:
15
32
  """Extra information returned by the ACME server."""
@@ -31,7 +48,39 @@ class LEGOResponse:
31
48
 
32
49
 
33
50
  class LEGOError(Exception):
34
- """Exceptions that are returned from the LEGO Go library."""
51
+ """Unified exception for all errors returned by the lego invocation.
52
+
53
+ Attributes:
54
+ type: source of the error. "acme" when coming from the ACME server, otherwise "lego".
55
+ code: error code/category. For ACME, this is derived from the ACME problem type; otherwise, it's set by lego.
56
+ status: HTTP status code for ACME errors, None otherwise.
57
+ detail: human-readable description of the error.
58
+ acme_type: full ACME problem type (URN), present only for ACME errors.
59
+ subproblems: list of Subproblem objects with detailed error information.
60
+ info: dictionary with the raw error information returned by the underlying call.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ detail: str,
66
+ *,
67
+ type: str = "lego",
68
+ code: str = "",
69
+ status: int | None = None,
70
+ acme_type: str = "",
71
+ subproblems: list[Subproblem] | None = None,
72
+ info: dict | None = None,
73
+ ):
74
+ # Include code in exception message for better error display
75
+ message = f"[{code}] {detail}" if code else detail
76
+ super().__init__(message)
77
+ self.type = type
78
+ self.code = code
79
+ self.status = status
80
+ self.detail = detail
81
+ self.acme_type = acme_type
82
+ self.subproblems = subproblems or []
83
+ self.info = info or {}
35
84
 
36
85
 
37
86
  def run_lego_command(
@@ -77,7 +126,42 @@ def run_lego_command(
77
126
  "utf-8",
78
127
  )
79
128
  result: bytes = library.RunLegoCommand(message)
80
- if result.startswith(b"error:"):
81
- raise LEGOError(result.decode())
82
- result_dict = json.loads(result.decode("utf-8"))
83
- return LEGOResponse(**{**result_dict, "metadata": Metadata(**result_dict.get("metadata"))})
129
+ result_str = result.decode("utf-8")
130
+
131
+ try:
132
+ result_dict = json.loads(result_str)
133
+ except json.JSONDecodeError as e:
134
+ raise LEGOError(f"Failed to parse response: {result_str}") from e
135
+
136
+ if not result_dict.get("success", False):
137
+ error_info: dict = result_dict.get("error", {})
138
+ err_source = error_info.get("type", "lego")
139
+ detail = error_info.get("detail", "Unknown error occurred")
140
+
141
+ subproblems = []
142
+ for sub_dict in error_info.get("subproblems", []):
143
+ identifier_dict = sub_dict.get("identifier", {})
144
+ subproblems.append(
145
+ Subproblem(
146
+ type=sub_dict.get("type", ""),
147
+ detail=sub_dict.get("detail", ""),
148
+ identifier=Identifier(
149
+ type=identifier_dict.get("type", ""),
150
+ value=identifier_dict.get("value", ""),
151
+ ),
152
+ )
153
+ )
154
+
155
+ info = dict(error_info)
156
+ raise LEGOError(
157
+ detail,
158
+ type="acme" if err_source == "acme" else "lego",
159
+ code=error_info.get("code", ""),
160
+ status=error_info.get("status"),
161
+ acme_type=error_info.get("acme_type", "") if err_source == "acme" else "",
162
+ subproblems=subproblems,
163
+ info=info,
164
+ )
165
+
166
+ data = result_dict.get("data", {})
167
+ return LEGOResponse(**{**data, "metadata": Metadata(**data.get("metadata", {}))})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylego
3
- Version: 0.1.32
3
+ Version: 0.1.34
4
4
  Summary: A python wrapper package for the lego application written in Golang
5
5
  Author-email: Canonical <telco-engineers@lists.canonical.com>
6
6
  Project-URL: Homepage, https://github.com/canonical/pylego
@@ -63,6 +63,29 @@ On top of the environment variables that LEGO supports, we have some extra ones
63
63
  | `TLSALPN01_IFACE` | Interface for the TLS-ALPN-01 challenge (when `plugin=tls`). Any interface by default. |
64
64
  | `TLSALPN01_PORT` | Port for the TLS-ALPN-01 challenge (when `plugin=tls`). 443 by default. |
65
65
 
66
+ ## Error Handling
67
+
68
+ All errors raised by `run_lego_command()` are `LEGOError` exceptions with structured information:
69
+
70
+ ```python
71
+ from pylego import run_lego_command, LEGOError
72
+
73
+ try:
74
+ result = run_lego_command(...)
75
+ except LEGOError as e:
76
+ print(f"Error: {e}") # Includes error code in message
77
+ print(f"Type: {e.type}") # "acme" (server) or "lego" (client)
78
+ print(f"Code: {e.code}") # e.g., "invalid_csr", "dns_provider_failed"
79
+ print(f"Detail: {e.detail}") # Human-readable message
80
+
81
+ # ACME-specific fields
82
+ if e.type == "acme":
83
+ print(f"Status: {e.status}") # HTTP status code
84
+ print(f"Subproblems: {e.subproblems}") # Validation details per domain
85
+ ```
86
+
87
+ Common error codes: `invalid_csr`, `invalid_private_key`, `dns_provider_failed`, `network_error`, `certificate_obtain_failed`. ACME errors include codes like `unauthorized`, `rateLimited`, `dns`.
88
+
66
89
  ## How does it work?
67
90
 
68
91
  Golang supports building a shared c library from its CLI build tool. We import and use the LEGO application from GoLang, and provide a stub with C bindings so that the shared C binary we produce exposes a C API for other programs to import and utilize. pylego then uses the [ctypes](https://docs.python.org/3/library/ctypes.html) standard library in python to load this binary, and make calls to its methods.
@@ -0,0 +1,11 @@
1
+ pylego/__init__.py,sha256=7rcUcQcOWsOLxTOEXF2ASkwm_7eED1UIXzxdlgKPr5c,82
2
+ pylego/go.mod,sha256=qLNIZo1UcJ9msJWbn7CYkfi8rsKi29FfI0tjnw1GA4E,11485
3
+ pylego/go.sum,sha256=o_rnHFfbUwYJGixlas9y-yLlNgkBQWuWfRVTN8Xinyc,159459
4
+ pylego/lego.go,sha256=zauvVfyFVfH_4Z1lAiTAYRt-tU-0Z4_xmVjKhkP7uu8,11180
5
+ pylego/lego.so,sha256=wpSLjV_V1S5ql25Smea_jUYGEQ9jow1XHZ7lgeQWFYw,87331720
6
+ pylego/pylego.py,sha256=556axo5EwOUGG5ZtsHgfFog-CTkJtOsavdTGLgEAu6k,5522
7
+ pylego-0.1.34.dist-info/licenses/LICENSE,sha256=aklz9Y8CIpFsN61U4jHlJYp4W_8HoDpY-tINlDcdSZY,10934
8
+ pylego-0.1.34.dist-info/METADATA,sha256=CWz5i5KWYzGdEAxVUlJk7AUaNM71y1TwAaTEWszxpds,6703
9
+ pylego-0.1.34.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ pylego-0.1.34.dist-info/top_level.txt,sha256=pSOYv55_w90qy3xOvqz_ysSz-X-XRTb-jMpiOyLNnNs,7
11
+ pylego-0.1.34.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- pylego/__init__.py,sha256=7rcUcQcOWsOLxTOEXF2ASkwm_7eED1UIXzxdlgKPr5c,82
2
- pylego/go.mod,sha256=2zddNtfcY9OT_qrCABQwRB06dvL0QF9kMA1E6lmvOlM,12467
3
- pylego/go.sum,sha256=-Gl6lIAbKb0cjJHr4dVvA4_XGBSHONqL0lKRGHUoYn0,187420
4
- pylego/lego.go,sha256=Fw9klKd_4pQV6SKn7O2WXLs-M_ta_l5nxZhluov0EeU,6509
5
- pylego/lego.so,sha256=iY76dyNQa-lhximqoLrdTJPJ7ATqfbo_z1xncZEbprM,87362384
6
- pylego/pylego.py,sha256=HgNEnKndDHskz5SoB3tOdSDNyd73E9X57Y3Ksvfog1Q,2609
7
- pylego-0.1.32.dist-info/licenses/LICENSE,sha256=aklz9Y8CIpFsN61U4jHlJYp4W_8HoDpY-tINlDcdSZY,10934
8
- pylego-0.1.32.dist-info/METADATA,sha256=hX8nkvZacF4-eZZk5Y_2TGxKWZ_ZJgNywHy6c3rtmXM,5776
9
- pylego-0.1.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- pylego-0.1.32.dist-info/top_level.txt,sha256=pSOYv55_w90qy3xOvqz_ysSz-X-XRTb-jMpiOyLNnNs,7
11
- pylego-0.1.32.dist-info/RECORD,,